From 7821d44429e1c4ac00d5056b9ed4f22f7a2602f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Tue, 21 Jan 2025 12:02:35 +0100 Subject: [PATCH 01/18] fix(searchabeldropdown): support for selectedId and multiple selections --- .../src/dropdown/element.ts | 10 +- .../src/provider/controller.ts | 139 ++++++++++-------- .../input/searchable-dropdown.stories.ts | 7 +- 3 files changed, 92 insertions(+), 64 deletions(-) diff --git a/packages/searchable-dropdown/src/dropdown/element.ts b/packages/searchable-dropdown/src/dropdown/element.ts index 82ee29929..e4090ca21 100644 --- a/packages/searchable-dropdown/src/dropdown/element.ts +++ b/packages/searchable-dropdown/src/dropdown/element.ts @@ -131,12 +131,18 @@ export class SearchableDropdownElement @query('fwc-list') listElement: ListElement | undefined; + updated(props: Map) { + if (props.has('selectedId')) { + this.controller.initialItemsMutation(); + } + } + /* Build fwc-list-items */ protected buildListItem(item: SearchableDropdownResultItem): HTMLTemplateResult { this.controller._listItems.push(item.id); const itemClasses = { 'list-item': true, - 'item-selected': !!item.isSelected, + 'item-selected': !!item.isSelected || !!this.controller._selectedItems.find((si) => si.id === item.id), 'item-error': !!item.isError, }; @@ -286,7 +292,7 @@ export class SearchableDropdownElement item.title).join(', ')} name="searchabledropdown" variant=${variant} disabled=${ifDefined(disabled)} diff --git a/packages/searchable-dropdown/src/provider/controller.ts b/packages/searchable-dropdown/src/provider/controller.ts index e8c74fc9e..05dce2cde 100644 --- a/packages/searchable-dropdown/src/provider/controller.ts +++ b/packages/searchable-dropdown/src/provider/controller.ts @@ -5,7 +5,6 @@ import { SearchableDropdownResult, SearchableDropdownResolver, SearchableDropdownControllerHost, - SearchableDropdownResultItem, SearchableDropdownSelectEvent, } from '../types'; import { SearchableDropdownConnectEvent, ExplicitEventTarget } from '../types'; @@ -49,8 +48,8 @@ export class SearchableDropdownController implements ReactiveController { } else { result = await this.resolver.searchQuery(qs); } - // set isSelected on result items - this.result = this.mutateResult(result); + + this.result = result; return this.result; }, () => [this.#queryString], @@ -134,36 +133,58 @@ export class SearchableDropdownController implements ReactiveController { }; /** - * Mutates result to set parameters like isSelected. - * @param result SearchableDropdownResult - * @returns result + * Set/clear selected id in dropdown list + * @param selectedI */ - private mutateResult(result: SearchableDropdownResult) { - if (result) { - const { selectedId } = this.#host; - for (let i = 0; i < result.length; i++) { - const item = result[i]; - - if (item.type === 'section' && item.children?.length) { - for (let x = 0; x < item.children.length; x++) { - const kid = item.children[x]; - if (this._selectedItems.find((s) => s.id === kid.id) || selectedId === kid.id) { - kid.isSelected = true; + private setSelectedItem(selectedId: string | null) { + const isMultiple = this.#host.multiple; + + this.result = this.result?.map((item) => { + if (item.type === 'section' && item.children?.length) { + item.children = item.children.map((child) => { + if (selectedId === child.id) { + child.isSelected = true; + if (isMultiple) { + this._selectedItems.push(child); } else { - kid.isSelected = false; + this._selectedItems = [child]; } + } else if (!isMultiple) { + child.isSelected = false; } - } else { - if (this._selectedItems.find((s) => s.id === item.id) || selectedId === item.id) { - item.isSelected = true; + return child; + }); + } else { + if (selectedId === item.id) {'' + item.isSelected = true; + if (isMultiple) { + this._selectedItems.push(item); } else { - item.isSelected = false; + this._selectedItems = [item]; } + } else if (!isMultiple) { + item.isSelected = false; } } - } - return result; + return item; + }); + } + + /** + * Mutates the initialResult to set parameters like isSelected. + * @returns SearchableDropdownResult + */ + public initialItemsMutation() { + const { selectedId } = this.#host; + + this.setSelectedItem(selectedId ?? null); + + // clear any selected items on null + if (selectedId === null) { + this._selectedItems = []; + this.#host.requestUpdate(); + } } /** @@ -185,22 +206,13 @@ export class SearchableDropdownController implements ReactiveController { const id = this._listItems[event.detail.index]; /* Find selected item in resolver result list */ - let selectedItem: SearchableDropdownResultItem | undefined; - - // get selected item from result - for (const item of this.result) { - if (item.id === id) { - selectedItem = item; - break; - } else if (item.children) { - for (const childItem of item.children) { - if (childItem.id === id) { - selectedItem = childItem; - break; - } - } + const selectedItem = this.result.find((item) => { + if (item.children?.length) { + return item.children.find((child) => child.id === id); } - } + + return item.id === id; + }); /* Set Error if none matched the resolver result */ if (!selectedItem?.id) { @@ -208,28 +220,41 @@ export class SearchableDropdownController implements ReactiveController { } /* Set active state and save selected item in state */ - if (this.#host.multiple) { - if (this._selectedItems.find((si) => si.id === selectedItem?.id)) { - /* Already selected so clear it from selections */ - selectedItem.isSelected = false; - this._selectedItems = this._selectedItems.filter((i) => i.id !== selectedItem?.id); - this.#host.value = ''; + if (this._selectedItems.find((si) => si.id === selectedItem?.id)) { + /* Already selected so clear it from selections */ + selectedItem.isSelected = false; + this._selectedItems = this._selectedItems.filter((i) => i.id !== selectedItem?.id); + } else { + /* Adds new item to selections */ + if (this.#host.multiple) { + this._selectedItems.push(selectedItem); } else { - /* Adds new item to selections */ + // make sure all others are unactive + this.setSelectedItem(null); + // select only this item as active selectedItem.isSelected = true; - this._selectedItems.push(selectedItem); - this.#host.value = selectedItem?.title || ''; + + this._selectedItems = [selectedItem]; } - } else { - /* Adds new item to selections */ - this._selectedItems = [selectedItem]; - this.#host.value = selectedItem?.title || ''; } + + // if (this.#host.multiple) { + // if (this._selectedItems.find((si) => si.id === selectedItem?.id)) { + // /* Already selected so clear it from selections */ + // selectedItem.isSelected = false; + // this._selectedItems = this._selectedItems.filter((i) => i.id !== selectedItem?.id); + // } else { + // /* Adds new item to selections */ + // selectedItem.isSelected = true; + // this._selectedItems.push(selectedItem); + // } + // } else { + // /* Adds new item to selections */ + // this._selectedItems = [selectedItem]; + // } } else { - /* FALSE === this.result && this._listItems */ - /* Clear selected states */ + /* Clear selected states if any */ this._selectedItems = []; - this.#host.value = ''; } if (!this.#host.multiple) { @@ -246,9 +271,6 @@ export class SearchableDropdownController implements ReactiveController { }), ); - /* Sets items isSelected in task */ - this.task.run(); - /* Refresh host */ this.#host.requestUpdate(); } @@ -278,7 +300,6 @@ export class SearchableDropdownController implements ReactiveController { this.#host.textInputElement.blur(); } - this.#host.value = ''; this._selectedItems = []; this.#queryString = ''; diff --git a/storybook/stories/input/searchable-dropdown.stories.ts b/storybook/stories/input/searchable-dropdown.stories.ts index 7eae78adc..295e78c11 100644 --- a/storybook/stories/input/searchable-dropdown.stories.ts +++ b/storybook/stories/input/searchable-dropdown.stories.ts @@ -28,6 +28,7 @@ const render = (props: SearchableDropdownProps) => html` placeholder="${ifDefined(props.placeholder)}" leadingIcon="${ifDefined(props.leadingIcon)}" multiple="${ifDefined(props.multiple)}" + selectedId="${ifDefined(props.selectedId)}" select-text-on-focus="${ifDefined(props.selectTextOnFocus)}" > `; @@ -39,17 +40,17 @@ export const Default: Story = { export const Label: Story = { ...Default, - render: (props) => render({ ...props, label: "Label" }), + render: (props) => render({ ...props, label: 'Label' }), }; export const Placeholder: Story = { ...Default, - render: (props) => render({ ...props, placeholder: "Placeholder" }), + render: (props) => render({ ...props, placeholder: 'Placeholder' }), }; export const LeadingIcon: Story = { ...Default, - render: (props) => render({ ...props, leadingIcon: "list" }), + render: (props) => render({ ...props, leadingIcon: 'list' }), }; export const Multiple: Story = { From debdbcc4fb170ab36f72158ebbd61397b9e7a140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Tue, 21 Jan 2025 12:02:50 +0100 Subject: [PATCH 02/18] chore: changeset --- .changeset/yellow-carrots-play.md | 13 +++++++++++++ .../src/provider/controller.ts | 17 +---------------- 2 files changed, 14 insertions(+), 16 deletions(-) create mode 100644 .changeset/yellow-carrots-play.md diff --git a/.changeset/yellow-carrots-play.md b/.changeset/yellow-carrots-play.md new file mode 100644 index 000000000..8d2aedb06 --- /dev/null +++ b/.changeset/yellow-carrots-play.md @@ -0,0 +1,13 @@ +--- +'@equinor/fusion-wc-searchable-dropdown': patch +'@equinor/fusion-wc-storybook': patch +--- + +### @equinor/fusion-wc-searchable-dropdown + +Fix: support for setting selectedId in initalItems +Fix: better handling of multiple selections + +### @equinor/fusion-wc-storybook + +Fix: can use selectedId in stories and eslint diff --git a/packages/searchable-dropdown/src/provider/controller.ts b/packages/searchable-dropdown/src/provider/controller.ts index 05dce2cde..0eb801c1e 100644 --- a/packages/searchable-dropdown/src/provider/controller.ts +++ b/packages/searchable-dropdown/src/provider/controller.ts @@ -155,7 +155,7 @@ export class SearchableDropdownController implements ReactiveController { return child; }); } else { - if (selectedId === item.id) {'' + if (selectedId === item.id) { item.isSelected = true; if (isMultiple) { this._selectedItems.push(item); @@ -237,21 +237,6 @@ export class SearchableDropdownController implements ReactiveController { this._selectedItems = [selectedItem]; } } - - // if (this.#host.multiple) { - // if (this._selectedItems.find((si) => si.id === selectedItem?.id)) { - // /* Already selected so clear it from selections */ - // selectedItem.isSelected = false; - // this._selectedItems = this._selectedItems.filter((i) => i.id !== selectedItem?.id); - // } else { - // /* Adds new item to selections */ - // selectedItem.isSelected = true; - // this._selectedItems.push(selectedItem); - // } - // } else { - // /* Adds new item to selections */ - // this._selectedItems = [selectedItem]; - // } } else { /* Clear selected states if any */ this._selectedItems = []; From f55680b048ca0d89394475116209e64b5b2ddd97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Wed, 22 Jan 2025 13:37:04 +0100 Subject: [PATCH 03/18] fix(searchable-dropdown): improving keep track of selectedItems --- .../src/dropdown/element.ts | 15 +- .../src/provider/controller.ts | 134 +++++++++++------- packages/searchable-dropdown/src/types.ts | 1 + 3 files changed, 95 insertions(+), 55 deletions(-) diff --git a/packages/searchable-dropdown/src/dropdown/element.ts b/packages/searchable-dropdown/src/dropdown/element.ts index e4090ca21..04a1056eb 100644 --- a/packages/searchable-dropdown/src/dropdown/element.ts +++ b/packages/searchable-dropdown/src/dropdown/element.ts @@ -1,7 +1,7 @@ -import { html, LitElement, type HTMLTemplateResult, type CSSResult } from 'lit'; +import { html, LitElement, type HTMLTemplateResult, type CSSResult, PropertyValues } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { unsafeSVG } from 'lit/directives/unsafe-svg.js'; -import { property } from 'lit/decorators.js'; +import { property, state } from 'lit/decorators.js'; import { query } from 'lit/decorators/query.js'; @@ -131,7 +131,10 @@ export class SearchableDropdownElement @query('fwc-list') listElement: ListElement | undefined; - updated(props: Map) { + @state() + selectedItems: Set = new Set([]); + + updated(props: PropertyValues) { if (props.has('selectedId')) { this.controller.initialItemsMutation(); } @@ -142,7 +145,7 @@ export class SearchableDropdownElement this.controller._listItems.push(item.id); const itemClasses = { 'list-item': true, - 'item-selected': !!item.isSelected || !!this.controller._selectedItems.find((si) => si.id === item.id), + 'item-selected': !!item.isSelected, 'item-error': !!item.isError, }; @@ -292,7 +295,7 @@ export class SearchableDropdownElement item.title).join(', ')} + value=${this.value} name="searchabledropdown" variant=${variant} disabled=${ifDefined(disabled)} @@ -303,7 +306,7 @@ export class SearchableDropdownElement this.controller.isOpen = true; this.selectTextOnFocus && this.textInputElement?.select(); }} - @keyup=${this.controller.handleKeyup} + @input=${this.controller.handleKeyup} > diff --git a/packages/searchable-dropdown/src/provider/controller.ts b/packages/searchable-dropdown/src/provider/controller.ts index 0eb801c1e..7132ed899 100644 --- a/packages/searchable-dropdown/src/provider/controller.ts +++ b/packages/searchable-dropdown/src/provider/controller.ts @@ -16,7 +16,6 @@ export class SearchableDropdownController implements ReactiveController { protected resolver?: SearchableDropdownResolver; public _listItems: Array = []; - public _selectedItems: SearchableDropdownResult = []; public result?: SearchableDropdownResult; public task!: Task<[string], SearchableDropdownResult>; @@ -50,6 +49,7 @@ export class SearchableDropdownController implements ReactiveController { } this.result = result; + this.setIsSelected(); return this.result; }, () => [this.#queryString], @@ -133,35 +133,24 @@ export class SearchableDropdownController implements ReactiveController { }; /** - * Set/clear selected id in dropdown list - * @param selectedI + * Loops over result items and sets isSelected to true on selected items + * based on items in selectedItems array. */ - private setSelectedItem(selectedId: string | null) { + private setIsSelected() { const isMultiple = this.#host.multiple; - this.result = this.result?.map((item) => { - if (item.type === 'section' && item.children?.length) { + if (item.children?.length) { item.children = item.children.map((child) => { - if (selectedId === child.id) { + if (this.#host.selectedItems.has(child.id)) { child.isSelected = true; - if (isMultiple) { - this._selectedItems.push(child); - } else { - this._selectedItems = [child]; - } } else if (!isMultiple) { child.isSelected = false; } return child; }); } else { - if (selectedId === item.id) { + if (this.#host.selectedItems.has(item.id)) { item.isSelected = true; - if (isMultiple) { - this._selectedItems.push(item); - } else { - this._selectedItems = [item]; - } } else if (!isMultiple) { item.isSelected = false; } @@ -172,19 +161,58 @@ export class SearchableDropdownController implements ReactiveController { } /** - * Mutates the initialResult to set parameters like isSelected. - * @returns SearchableDropdownResult + * Mutates the initialResult to set parameters like isSelected and selectedItems. */ public initialItemsMutation() { const { selectedId } = this.#host; - this.setSelectedItem(selectedId ?? null); + // clear any previous selectedItems when changing property + this.#host.selectedItems.clear(); - // clear any selected items on null - if (selectedId === null) { - this._selectedItems = []; - this.#host.requestUpdate(); + // set selectedItems based on selectedId + if (selectedId !== null) { + this.#host.selectedItems.clear(); + this.result?.forEach((item) => { + if (item.children?.length) { + item.children.forEach((child) => { + if (selectedId === child.id) { + this.#host.selectedItems.add(child.id); + } + }); + } else { + if (selectedId === item.id) { + this.#host.selectedItems.add(item.id); + } + } + }); } + + //set input value based on selectedItems + this.#host.value = this.allSelectedItems.map((item) => item.title).join(', '); + + // update isSelected in dropdown list + this.setIsSelected(); + } + + /** + * Get selectedItems objects from result array. + */ + public get allSelectedItems(): SearchableDropdownResult { + const selectedItems: SearchableDropdownResult = []; + this.result?.forEach((item) => { + if (item.children?.length) { + item.children.forEach((child) => { + if (this.#host.selectedItems.has(child.id)) { + selectedItems.push(child); + } + }); + } + if (this.#host.selectedItems.has(item.id)) { + selectedItems.push(item); + } + }); + + return selectedItems; } /** @@ -206,42 +234,47 @@ export class SearchableDropdownController implements ReactiveController { const id = this._listItems[event.detail.index]; /* Find selected item in resolver result list */ - const selectedItem = this.result.find((item) => { - if (item.children?.length) { - return item.children.find((child) => child.id === id); - } + const selectedItem = this.result + .map((item) => { + if (item.children) { + const match = item.children.find((kid) => kid.id === id); + if (match) { + return match; + } + } - return item.id === id; - }); + return item.id === id ? item : null; + }) + .filter((i) => i !== null) + .pop(); /* Set Error if none matched the resolver result */ - if (!selectedItem?.id) { + if (!selectedItem) { throw new Error('SearchableDropdownController could not find a match in result provided by resolver.'); } /* Set active state and save selected item in state */ - if (this._selectedItems.find((si) => si.id === selectedItem?.id)) { - /* Already selected so clear it from selections */ - selectedItem.isSelected = false; - this._selectedItems = this._selectedItems.filter((i) => i.id !== selectedItem?.id); + if (this.#host.selectedItems.has(selectedItem.id)) { + /* Unselecting */ + this.#host.selectedItems.delete(selectedItem.id); } else { /* Adds new item to selections */ - if (this.#host.multiple) { - this._selectedItems.push(selectedItem); - } else { - // make sure all others are unactive - this.setSelectedItem(null); - // select only this item as active - selectedItem.isSelected = true; - - this._selectedItems = [selectedItem]; + if (!this.#host.multiple) { + this.#host.selectedItems.clear(); } + this.#host.selectedItems.add(selectedItem.id); } } else { /* Clear selected states if any */ - this._selectedItems = []; + this.#host.selectedItems.clear(); } + // set isSelected based on selectedItems in dropdown list + this.setIsSelected(); + + // set input value based on selectedItems + this.#host.value = this.allSelectedItems.map((item) => item.title).join(', '); + if (!this.#host.multiple) { this.isOpen = false; } @@ -250,7 +283,7 @@ export class SearchableDropdownController implements ReactiveController { this.#host.dispatchEvent( new SearchableDropdownSelectEvent({ detail: { - selected: this._selectedItems, + selected: this.allSelectedItems, }, bubbles: true, }), @@ -285,7 +318,9 @@ export class SearchableDropdownController implements ReactiveController { this.#host.textInputElement.blur(); } - this._selectedItems = []; + this.#host.selectedItems.clear(); + this.#host.value = ''; + this.#queryString = ''; /* also runs task */ @@ -314,7 +349,7 @@ export class SearchableDropdownController implements ReactiveController { this.#host.trailingIcon = state ? 'close' : ''; /* syncs dropdown list with textinput */ - if (this._selectedItems) { + if (this.#host.selectedItems.size) { this.task.run(); } @@ -357,6 +392,7 @@ export class SearchableDropdownController implements ReactiveController { if (this.timer) { clearTimeout(this.timer); } + this.timer = setTimeout(() => { this.#queryString = target.value.trim().toLowerCase(); this.#host.requestUpdate(); diff --git a/packages/searchable-dropdown/src/types.ts b/packages/searchable-dropdown/src/types.ts index 5944d1cc6..de27f28a1 100644 --- a/packages/searchable-dropdown/src/types.ts +++ b/packages/searchable-dropdown/src/types.ts @@ -89,6 +89,7 @@ export interface SearchableDropdownControllerHost extends SearchableDropdownProp renderRoot: HTMLElement | DocumentFragment; trailingIcon: string; trailingIconElement?: IconElement; + selectedItems: Set; id: string; } From a6c52edb37286c76e6708c730ef874d4045adb46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Wed, 22 Jan 2025 14:04:00 +0100 Subject: [PATCH 04/18] fix(searchable-dropdown): removing styles from @material --- packages/searchable-dropdown/src/dropdown/element.css.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/searchable-dropdown/src/dropdown/element.css.ts b/packages/searchable-dropdown/src/dropdown/element.css.ts index 385cf0cb2..c1f9d9ae4 100644 --- a/packages/searchable-dropdown/src/dropdown/element.css.ts +++ b/packages/searchable-dropdown/src/dropdown/element.css.ts @@ -1,11 +1,5 @@ import { css, unsafeCSS, type CSSResult } from 'lit'; import { styles as theme } from '@equinor/fusion-web-theme'; -/** - * @todo - * @eikeland - * Why this import, shouldn`t this come from @equinor/fusion-wc-textfield? - */ -import { styles as mdcStyle } from '@material/mwc-textfield/mwc-textfield.css'; export const fwcsdd: CSSResult = css` :host { @@ -135,6 +129,6 @@ export const fwcsdd: CSSResult = css` } `; -export const sddStyles: CSSResult[] = [mdcStyle, fwcsdd]; +export const sddStyles: CSSResult[] = [fwcsdd]; export default sddStyles; From b7f13ea3cb827cb0d042c61d260e2d246042b933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Wed, 22 Jan 2025 13:59:12 +0100 Subject: [PATCH 05/18] fix(person-select): removing dependency to searchable-dropdown --- packages/person/package.json | 1 - .../src/components/select/controller.ts | 12 +- .../src/components/select/element.css.ts | 135 +++++++++++++++++- .../person/src/components/select/element.ts | 7 +- 4 files changed, 143 insertions(+), 12 deletions(-) diff --git a/packages/person/package.json b/packages/person/package.json index 9590baef7..942a99885 100644 --- a/packages/person/package.json +++ b/packages/person/package.json @@ -70,7 +70,6 @@ "@equinor/fusion-wc-core": "workspace:^", "@equinor/fusion-wc-icon": "workspace:^", "@equinor/fusion-wc-list": "workspace:^", - "@equinor/fusion-wc-searchable-dropdown": "workspace:^", "@equinor/fusion-wc-skeleton": "workspace:^", "@equinor/fusion-wc-textinput": "workspace:^", "@equinor/fusion-web-theme": "^0.1.10", diff --git a/packages/person/src/components/select/controller.ts b/packages/person/src/components/select/controller.ts index b4f7b789b..71e0e6268 100644 --- a/packages/person/src/components/select/controller.ts +++ b/packages/person/src/components/select/controller.ts @@ -1,9 +1,15 @@ import { ReactiveController } from 'lit'; import { PersonSelectElement } from './element'; -import { ExplicitEventTarget } from '@equinor/fusion-wc-searchable-dropdown'; import { PersonInfo } from '../../types'; +export interface ExplicitEventTarget extends Event { + readonly detail: { + index: number; + }; + readonly explicitOriginalTarget: HTMLInputElement; +} + type PersonSelectEventDetail = { selected: PersonInfo | null; }; @@ -62,7 +68,7 @@ export class PersonSelectController implements ReactiveController { } return selectedPerson; })(); - + // there are no selected person if (selectedPersonId === undefined) return; @@ -74,7 +80,7 @@ export class PersonSelectController implements ReactiveController { this.#host.upn = selectedPersonId; this.#host.azureId = undefined; return; - } + } // check if azureId is a valid guid if (selectedPersonId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i)) { this.#host.azureId = selectedPersonId; diff --git a/packages/person/src/components/select/element.css.ts b/packages/person/src/components/select/element.css.ts index c1eb90135..eb4604e11 100644 --- a/packages/person/src/components/select/element.css.ts +++ b/packages/person/src/components/select/element.css.ts @@ -1,5 +1,136 @@ -import { css, type CSSResult } from 'lit'; -import { sddStyles } from '@equinor/fusion-wc-searchable-dropdown'; +import { css, unsafeCSS, type CSSResult } from 'lit'; + +import { styles as theme } from '@equinor/fusion-web-theme'; + +export const fwcsdd: CSSResult = css` + :host { + position: relative; + width: 100%; + --textinput-dense-size: 32px; + --fwc-list-item-vertical-padding: 0.5rem; + --fwc-text-field-base-color: ${unsafeCSS(theme.colors.text.static_icons__tertiary.getVariable('color'))}; + --fwc-text-field-fill-color: ${unsafeCSS(theme.colors.ui.background__light.getVariable('color'))}; + --fwc-text-field-ink-color: ${unsafeCSS(theme.colors.text.static_icons__default.getVariable('color'))}; + --fwc-text-field-disabled-ink-color: ${unsafeCSS(theme.colors.text.static_icons__default.getVariable('color'))}; + } + .input { + posistion: relative; + } + fwc-textinput { + width: 100%; + } + fwc-textinput[disabled] { + opacity: 0.5; + } + fwc-textinput[dense] { + --mdc-text-field-outlined-idle-border-color: transparent; + --mdc-shape-small: 0; + --mdc-text-field-outlined-hover-border-color: ${unsafeCSS( + theme.colors.interactive.primary__resting.getVariable('color'), + )}; + } + .interactive { + cursor: pointer; + } + [slot='trailing'] { + display: block; + position: absolute; + top: 2px; + right: 2px; + bottom: 2px; + display: flex; + justify-content: center; + align-items: center; + width: var(--textinput-dense-size); + color: ${unsafeCSS(theme.colors.interactive.primary__resting.getVariable('color'))}; + font-size: 0.8em; + } + .list { + position: absolute; + top: calc(100% + 4px); + left: 0; + width: 100%; + height: auto; + overflow: hidden; + z-index: 99; + box-shadow: + 0px 1px 5px rgba(0, 0, 0, 0.2), + 0px 3px 4px rgba(0, 0, 0, 0.12), + 0px 2px 4px rgba(0, 0, 0, 0.14); + border-radius: 4px; + } + .list-scroll { + width: 100%; + height: auto; + overflow: hidden auto; + scrollbar-width: thin; + scrollbar-color: ${unsafeCSS(theme.colors.ui.background__medium.getVariable('color'))} transparent; + } + fwc-list { + background-color: ${unsafeCSS(theme.colors.ui.background__light.getVariable('color'))}; + } + .variant-outlined fwc-list { + background-color: ${unsafeCSS(theme.colors.ui.background__default.getVariable('color'))}; + } + fwc-list-item { + --fwc-list-item-vertical-padding: 0.5rem; + } + fwc-list-item[disabled] { + background-color: ${unsafeCSS(theme.colors.ui.background__light.getVariable('color'))}; + } + fwc-list-item[disabled] [slot='graphic'], + fwc-list-item[disabled] [slot='meta'], + fwc-list-item[disabled] .item-text { + opacity: 0.6; + } + fwc-list-item[twoline] [slot='graphic'] svg { + font-size: 1.5rem; + } + .item-text { + display: flex; + flex-direction: column; + font-size: 1em; + } + .item-title { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + font-size: 1em; + line-height: 1.6; + } + .item-subtitle { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + font-size: 0.8em; + font-style: italic; + line-height: 1.6; + } + [slot='graphic'] { + display: flex; + } + [slot='graphic'] svg { + height: 1em; + width: auto; + } + fwc-divider { + display: flex; + align-items: center; + height: 1em; + } + .section-title { + font-weight: 600; + font-size: 16px; + padding: 0 1em; + margin: 1em 0 0; + } + .item-error { + --fwc-list-item-ink-color: ${unsafeCSS(theme.colors.interactive.danger__resting.getVariable('color'))}; + color: ${unsafeCSS(theme.colors.interactive.danger__resting.getVariable('color'))}; + } +`; + +const sddStyles: CSSResult[] = [fwcsdd]; // TODO - maybe this styling should be changed in parent! export const styles: CSSResult[] = [ diff --git a/packages/person/src/components/select/element.ts b/packages/person/src/components/select/element.ts index 5bd05c4be..5945254e3 100644 --- a/packages/person/src/components/select/element.ts +++ b/packages/person/src/components/select/element.ts @@ -12,8 +12,6 @@ import { PersonSelectController } from './controller'; import { styles as psStyles } from './element.css'; import { PersonSearchTask, PersonSearchControllerHost, PersonInfoTask } from '../../tasks'; -import { SearchableDropdownControllerHost } from '@equinor/fusion-wc-searchable-dropdown'; - import type { PersonInfo, PersonSearchResult } from '../../types'; import type { SelectedPersonProp } from './index'; @@ -58,10 +56,7 @@ PersonAvatarElement; * interface renderListItems(items: TResult): HTMLTemplateResult; * ``` */ -export class PersonSelectElement - extends LitElement - implements SearchableDropdownControllerHost, PersonSearchControllerHost, SelectedPersonProp -{ +export class PersonSelectElement extends LitElement implements PersonSearchControllerHost, SelectedPersonProp { /* style object css */ static styles: CSSResult[] = psStyles; From e8cfb211297dfe832302f57a7f80c63bd2080972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Wed, 22 Jan 2025 14:21:34 +0100 Subject: [PATCH 06/18] chore: changeset --- .changeset/funny-masks-laugh.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/funny-masks-laugh.md diff --git a/.changeset/funny-masks-laugh.md b/.changeset/funny-masks-laugh.md new file mode 100644 index 000000000..f9ea57003 --- /dev/null +++ b/.changeset/funny-masks-laugh.md @@ -0,0 +1,5 @@ +--- +'@equinor/fusion-wc-person': patch +--- + +Remove dependency for searchable-dropdown component \ No newline at end of file From 65f5eff53ac586a9a8581e1fba78c3c162769092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Thu, 23 Jan 2025 08:44:32 +0100 Subject: [PATCH 07/18] fix(sdd): remove doubble clear selectedItems --- .../src/provider/controller.ts | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/searchable-dropdown/src/provider/controller.ts b/packages/searchable-dropdown/src/provider/controller.ts index 7132ed899..e32feb094 100644 --- a/packages/searchable-dropdown/src/provider/controller.ts +++ b/packages/searchable-dropdown/src/provider/controller.ts @@ -171,7 +171,6 @@ export class SearchableDropdownController implements ReactiveController { // set selectedItems based on selectedId if (selectedId !== null) { - this.#host.selectedItems.clear(); this.result?.forEach((item) => { if (item.children?.length) { item.children.forEach((child) => { @@ -234,19 +233,13 @@ export class SearchableDropdownController implements ReactiveController { const id = this._listItems[event.detail.index]; /* Find selected item in resolver result list */ - const selectedItem = this.result - .map((item) => { - if (item.children) { - const match = item.children.find((kid) => kid.id === id); - if (match) { - return match; - } - } - - return item.id === id ? item : null; - }) - .filter((i) => i !== null) - .pop(); + const items = this.result.filter((item) => !item.children); + const kids = this.result + .filter((item) => item.children) + .map((item) => item.children as SearchableDropdownResult) + .flat(); + const results = [...items, ...kids]; + const selectedItem = results.find((item) => item.id === id); /* Set Error if none matched the resolver result */ if (!selectedItem) { From 31165ab49f7822922a3ac9cfaace1bf18c3d3d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Thu, 23 Jan 2025 10:32:06 +0100 Subject: [PATCH 08/18] fix(sdd): improving methods in controller --- .changeset/yellow-carrots-play.md | 7 +- .../src/dropdown/element.ts | 32 ++-- .../src/provider/controller.ts | 154 +++++++----------- packages/searchable-dropdown/src/types.ts | 1 + 4 files changed, 79 insertions(+), 115 deletions(-) diff --git a/.changeset/yellow-carrots-play.md b/.changeset/yellow-carrots-play.md index 8d2aedb06..f7ad09453 100644 --- a/.changeset/yellow-carrots-play.md +++ b/.changeset/yellow-carrots-play.md @@ -5,8 +5,11 @@ ### @equinor/fusion-wc-searchable-dropdown -Fix: support for setting selectedId in initalItems -Fix: better handling of multiple selections +- Fix: support for setting selectedId in initalItems. +- Fix: better handling of multiple selections. +- Removed need for mutating result with isSelected, we now keep track of that in selectedItems set. +- Removed controllers _listItems array since we added getter method for flattening the elements to be able to select by index, and more easily loop over all list items. +- Renamed methods to align naming scheme with functionality. ### @equinor/fusion-wc-storybook diff --git a/packages/searchable-dropdown/src/dropdown/element.ts b/packages/searchable-dropdown/src/dropdown/element.ts index 04a1056eb..bd700e31f 100644 --- a/packages/searchable-dropdown/src/dropdown/element.ts +++ b/packages/searchable-dropdown/src/dropdown/element.ts @@ -43,12 +43,13 @@ import { sddStyles } from './element.css'; * @property {string} dropdownHeight Sets max-height of list so user can scroll trough results * @property {string} graphic Icon to show before each fwc-list-item. If you want an icon only on one list-item then use the meta property on the SearchableDropdownResultItem * @property {string} initialText Text to display in dropdown before/without querystring in fwc-textinput + * @property {string} noContentText Text to display in dropdown when no matches are found * @property {string} label Label for fwc-textinput element * @property {string} leadingIcon Leading Icon to display in fwc-text-input * @property {string} meta Icon to show after each fwc-list-item. If you want an icon only on one list-item then use the meta property on the SearchableDropdownResultItem * @property {string} multiple Able to select multiple items * @property {string} placeholder Placeholder text for fwc-textinput element - * @property {string} selectedId ID that should be highlighted in dropdown + * @property {string} selectedId ID that should be selected * @property {string} value value for TextInput element * @property {'page' | 'page-outlined' | 'page-dense' | 'header' | 'header-filled'} variant Set variant to header|page style * @@ -105,6 +106,9 @@ export class SearchableDropdownElement @property() initialText = 'Start typing to search'; + @property() + noContentText = 'No content found'; + /* The leading icon to display in fwc-textinput */ @property() multiple = false; @@ -136,16 +140,21 @@ export class SearchableDropdownElement updated(props: PropertyValues) { if (props.has('selectedId')) { - this.controller.initialItemsMutation(); + this.controller.updateSelectedByProp(); } } /* Build fwc-list-items */ protected buildListItem(item: SearchableDropdownResultItem): HTMLTemplateResult { - this.controller._listItems.push(item.id); + const disabled = item.isDisabled || item.isError ? true : undefined; + + const graphic = item.graphic ?? this.graphic; + + const isSelected = this.selectedItems.has(item.id) || undefined; + const itemClasses = { 'list-item': true, - 'item-selected': !!item.isSelected, + 'item-selected': !!isSelected, 'item-error': !!item.isError, }; @@ -156,11 +165,6 @@ export class SearchableDropdownElement `; - const disabled = item.isDisabled || item.isError ? true : undefined; - const selected = item.isSelected ? true : undefined; - - const graphic = item.graphic ?? this.graphic; - /* Sett checkmark on selected items */ if (item.meta === 'check') { return html` @@ -189,7 +193,7 @@ export class SearchableDropdownElement key=${item.id} class=${classMap(itemClasses)} disabled=${ifDefined(disabled)} - selected=${ifDefined(selected)} + selected=${ifDefined(isSelected)} twoline=${ifDefined(item.subTitle)} graphic=${graphic ? 'icon' : ''} ?hasMeta=${!!item.meta} @@ -231,12 +235,6 @@ export class SearchableDropdownElement > ${this.controller.task.render({ complete: (result: SearchableDropdownResult) => { - /* - * clear previous render items. - * we need to save rendered items in state to be able to select them by index from action event - */ - this.controller._listItems = []; - /* Loop over task result */ return result.map((item, index) => { if (item.type === 'section') { diff --git a/packages/searchable-dropdown/src/provider/controller.ts b/packages/searchable-dropdown/src/provider/controller.ts index e32feb094..7885e6ff1 100644 --- a/packages/searchable-dropdown/src/provider/controller.ts +++ b/packages/searchable-dropdown/src/provider/controller.ts @@ -15,7 +15,6 @@ export class SearchableDropdownController implements ReactiveController { protected _isOpen = false; protected resolver?: SearchableDropdownResolver; - public _listItems: Array = []; public result?: SearchableDropdownResult; public task!: Task<[string], SearchableDropdownResult>; @@ -46,11 +45,14 @@ export class SearchableDropdownController implements ReactiveController { } } else { result = await this.resolver.searchQuery(qs); + if (!result.length) { + result = [{ id: 'no-result', title: this.#host.noContentText, isDisabled: true }]; + } } this.result = result; - this.setIsSelected(); - return this.result; + + return result; }, () => [this.#queryString], ); @@ -132,38 +134,10 @@ export class SearchableDropdownController implements ReactiveController { } }; - /** - * Loops over result items and sets isSelected to true on selected items - * based on items in selectedItems array. - */ - private setIsSelected() { - const isMultiple = this.#host.multiple; - this.result = this.result?.map((item) => { - if (item.children?.length) { - item.children = item.children.map((child) => { - if (this.#host.selectedItems.has(child.id)) { - child.isSelected = true; - } else if (!isMultiple) { - child.isSelected = false; - } - return child; - }); - } else { - if (this.#host.selectedItems.has(item.id)) { - item.isSelected = true; - } else if (!isMultiple) { - item.isSelected = false; - } - } - - return item; - }); - } - /** * Mutates the initialResult to set parameters like isSelected and selectedItems. */ - public initialItemsMutation() { + public updateSelectedByProp() { const { selectedId } = this.#host; // clear any previous selectedItems when changing property @@ -171,47 +145,53 @@ export class SearchableDropdownController implements ReactiveController { // set selectedItems based on selectedId if (selectedId !== null) { - this.result?.forEach((item) => { - if (item.children?.length) { - item.children.forEach((child) => { - if (selectedId === child.id) { - this.#host.selectedItems.add(child.id); - } - }); - } else { - if (selectedId === item.id) { - this.#host.selectedItems.add(item.id); - } - } - }); + const results = this.flatResult(); + const item = results.find((item) => item.id === selectedId); + if (item) { + this.#host.selectedItems.add(item.id); + } } //set input value based on selectedItems this.#host.value = this.allSelectedItems.map((item) => item.title).join(', '); - - // update isSelected in dropdown list - this.setIsSelected(); } /** * Get selectedItems objects from result array. */ public get allSelectedItems(): SearchableDropdownResult { - const selectedItems: SearchableDropdownResult = []; - this.result?.forEach((item) => { - if (item.children?.length) { - item.children.forEach((child) => { - if (this.#host.selectedItems.has(child.id)) { - selectedItems.push(child); - } - }); + return this.flatResult().filter((item) => this.#host.selectedItems.has(item.id)); + } + + /** + * Helper that flattens result array to a single array. + * used to easily find selected items in result array and set isSelected by index. + * @important This method must return results in the same order as resolvers result + * to be able to use index to set selectedItems. + * @returns SearchableDropdownResult + */ + private flatResult(): SearchableDropdownResult { + if (!this.result) { + return []; + } + + /** + * Create SearchableDropdownResult with all toplevel and child items + * excluding dividers and sections. + */ + return this.result.reduce((acc: SearchableDropdownResult, item) => { + // do not add dividers to acc + if (item.type === 'divider') { + return acc; } - if (this.#host.selectedItems.has(item.id)) { - selectedItems.push(item); + + // add sectioned children flatlist + if (item.type === 'section' && item.children) { + return [...acc, ...item.children]; } - }); - return selectedItems; + return [...acc, item]; + }, []); } /** @@ -229,41 +209,27 @@ export class SearchableDropdownController implements ReactiveController { return; } - if (this.result && this._listItems) { - const id = this._listItems[event.detail.index]; - - /* Find selected item in resolver result list */ - const items = this.result.filter((item) => !item.children); - const kids = this.result - .filter((item) => item.children) - .map((item) => item.children as SearchableDropdownResult) - .flat(); - const results = [...items, ...kids]; - const selectedItem = results.find((item) => item.id === id); - - /* Set Error if none matched the resolver result */ - if (!selectedItem) { - throw new Error('SearchableDropdownController could not find a match in result provided by resolver.'); - } + // get a flat list of result to select by index + const results = this.flatResult(); + if (!results.length) { + throw new Error('SearchableDropdownController could not find result set in resolver'); + } - /* Set active state and save selected item in state */ - if (this.#host.selectedItems.has(selectedItem.id)) { - /* Unselecting */ - this.#host.selectedItems.delete(selectedItem.id); - } else { - /* Adds new item to selections */ - if (!this.#host.multiple) { - this.#host.selectedItems.clear(); - } - this.#host.selectedItems.add(selectedItem.id); - } - } else { - /* Clear selected states if any */ - this.#host.selectedItems.clear(); + // extract id from results by index + const { id } = results[event.detail.index]; + if (!id) { + throw new Error('SearchableDropdownController could not find selected item in resolver'); } - // set isSelected based on selectedItems in dropdown list - this.setIsSelected(); + /* De-select if already selected */ + if (this.#host.selectedItems.has(id)) { + this.#host.selectedItems.delete(id); + } else { + if (!this.#host.multiple) { + this.#host.selectedItems.clear(); + } + this.#host.selectedItems.add(id); + } // set input value based on selectedItems this.#host.value = this.allSelectedItems.map((item) => item.title).join(', '); @@ -281,9 +247,6 @@ export class SearchableDropdownController implements ReactiveController { bubbles: true, }), ); - - /* Refresh host */ - this.#host.requestUpdate(); } /** @@ -307,7 +270,6 @@ export class SearchableDropdownController implements ReactiveController { /* needed to clear user input */ if (this.#host.textInputElement) { - this.#host.textInputElement.value = ''; this.#host.textInputElement.blur(); } diff --git a/packages/searchable-dropdown/src/types.ts b/packages/searchable-dropdown/src/types.ts index de27f28a1..2a631762f 100644 --- a/packages/searchable-dropdown/src/types.ts +++ b/packages/searchable-dropdown/src/types.ts @@ -90,6 +90,7 @@ export interface SearchableDropdownControllerHost extends SearchableDropdownProp trailingIcon: string; trailingIconElement?: IconElement; selectedItems: Set; + noContentText: string; id: string; } From af2e488daf35440299294f446cbd2b1dd9438b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Thu, 23 Jan 2025 11:03:06 +0100 Subject: [PATCH 09/18] fix(sdd): show trailing icon on selectedItems --- .../src/dropdown/element.ts | 26 ++++++++++++------- .../src/provider/controller.ts | 24 ++++++++++------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/searchable-dropdown/src/dropdown/element.ts b/packages/searchable-dropdown/src/dropdown/element.ts index bd700e31f..db3b1e4ff 100644 --- a/packages/searchable-dropdown/src/dropdown/element.ts +++ b/packages/searchable-dropdown/src/dropdown/element.ts @@ -85,7 +85,7 @@ export class SearchableDropdownElement /* The trailing icon to display in fwc-textinput */ @property({ attribute: false, state: true }) - trailingIcon = ''; + trailingIcon = 'close'; @query('.trailing') trailingIconElement?: IconElement; @@ -139,6 +139,7 @@ export class SearchableDropdownElement selectedItems: Set = new Set([]); updated(props: PropertyValues) { + console.log('props', props); if (props.has('selectedId')) { this.controller.updateSelectedByProp(); } @@ -271,6 +272,19 @@ export class SearchableDropdownElement `; } + protected renderCloseIcon(): HTMLTemplateResult { + if (this.controller.isOpen || this.selectedItems.size) { + return html``; + } + + return html``; + } /** * The main render function * @returns HTMLTemplateResult @@ -307,15 +321,7 @@ export class SearchableDropdownElement @input=${this.controller.handleKeyup} > - - - + ${this.renderCloseIcon()}
diff --git a/packages/searchable-dropdown/src/provider/controller.ts b/packages/searchable-dropdown/src/provider/controller.ts index 7885e6ff1..368dc56b8 100644 --- a/packages/searchable-dropdown/src/provider/controller.ts +++ b/packages/searchable-dropdown/src/provider/controller.ts @@ -145,7 +145,7 @@ export class SearchableDropdownController implements ReactiveController { // set selectedItems based on selectedId if (selectedId !== null) { - const results = this.flatResult(); + const results = this.flatResult; const item = results.find((item) => item.id === selectedId); if (item) { this.#host.selectedItems.add(item.id); @@ -153,14 +153,21 @@ export class SearchableDropdownController implements ReactiveController { } //set input value based on selectedItems + this.setHostValue(); + } + + /** + * Set host value based on selectedItems + */ + private setHostValue(): void { this.#host.value = this.allSelectedItems.map((item) => item.title).join(', '); } /** * Get selectedItems objects from result array. */ - public get allSelectedItems(): SearchableDropdownResult { - return this.flatResult().filter((item) => this.#host.selectedItems.has(item.id)); + private get allSelectedItems(): SearchableDropdownResult { + return this.flatResult.filter((item) => this.#host.selectedItems.has(item.id)); } /** @@ -170,7 +177,7 @@ export class SearchableDropdownController implements ReactiveController { * to be able to use index to set selectedItems. * @returns SearchableDropdownResult */ - private flatResult(): SearchableDropdownResult { + private get flatResult(): SearchableDropdownResult { if (!this.result) { return []; } @@ -210,7 +217,7 @@ export class SearchableDropdownController implements ReactiveController { } // get a flat list of result to select by index - const results = this.flatResult(); + const results = this.flatResult; if (!results.length) { throw new Error('SearchableDropdownController could not find result set in resolver'); } @@ -232,7 +239,7 @@ export class SearchableDropdownController implements ReactiveController { } // set input value based on selectedItems - this.#host.value = this.allSelectedItems.map((item) => item.title).join(', '); + this.setHostValue(); if (!this.#host.multiple) { this.isOpen = false; @@ -274,7 +281,7 @@ export class SearchableDropdownController implements ReactiveController { } this.#host.selectedItems.clear(); - this.#host.value = ''; + this.setHostValue(); this.#queryString = ''; @@ -300,9 +307,6 @@ export class SearchableDropdownController implements ReactiveController { public set isOpen(state: boolean) { this._isOpen = state; - // toogle close icon - this.#host.trailingIcon = state ? 'close' : ''; - /* syncs dropdown list with textinput */ if (this.#host.selectedItems.size) { this.task.run(); From e2f9322b0e616624439aeeba0050e7998ec7283d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Thu, 23 Jan 2025 11:09:40 +0100 Subject: [PATCH 10/18] fix(person): removed unnneded styles export in select --- packages/person/src/components/select/element.css.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/person/src/components/select/element.css.ts b/packages/person/src/components/select/element.css.ts index eb4604e11..9a3b5bdf2 100644 --- a/packages/person/src/components/select/element.css.ts +++ b/packages/person/src/components/select/element.css.ts @@ -2,7 +2,7 @@ import { css, unsafeCSS, type CSSResult } from 'lit'; import { styles as theme } from '@equinor/fusion-web-theme'; -export const fwcsdd: CSSResult = css` +const fwcsdd: CSSResult = css` :host { position: relative; width: 100%; @@ -130,11 +130,9 @@ export const fwcsdd: CSSResult = css` } `; -const sddStyles: CSSResult[] = [fwcsdd]; - // TODO - maybe this styling should be changed in parent! export const styles: CSSResult[] = [ - ...sddStyles, + ...[fwcsdd], css` fwc-list { --fwc-list-side-padding: 0.5rem; From 664a31dbdb02d4d3eda5d69eda47df7bce30e2bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Thu, 23 Jan 2025 12:30:03 +0100 Subject: [PATCH 11/18] fix(sdd): now supports using eds meta icon icon name --- .../searchable-dropdown/src/dropdown/element.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/searchable-dropdown/src/dropdown/element.ts b/packages/searchable-dropdown/src/dropdown/element.ts index db3b1e4ff..7953d4853 100644 --- a/packages/searchable-dropdown/src/dropdown/element.ts +++ b/packages/searchable-dropdown/src/dropdown/element.ts @@ -139,7 +139,6 @@ export class SearchableDropdownElement selectedItems: Set = new Set([]); updated(props: PropertyValues) { - console.log('props', props); if (props.has('selectedId')) { this.controller.updateSelectedByProp(); } @@ -201,7 +200,7 @@ export class SearchableDropdownElement >
${this.renderItemGraphic(item)}
${text} -
${unsafeHTML(item.meta)}
+
${this.renderItemMeta(item)}
`; } @@ -219,6 +218,20 @@ export class SearchableDropdownElement } } + protected renderItemMeta(item: SearchableDropdownResultItem): ReturnType | void { + const { meta, graphicType } = item; + switch (graphicType) { + case 'inline-html': + return unsafeHTML(meta); + case 'inline-svg': + return unsafeSVG(meta); + default: + if (meta) { + return html``; + } + } + } + /** * Render the menu if state is open * @returns HTMLTemplateResult From 47ef679f3c88623d24311ae6572bb068281a4b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Thu, 23 Jan 2025 12:39:40 +0100 Subject: [PATCH 12/18] fix(sdd-story): provider typing and icon meta example --- storybook/stories/input/searchable-dropdown-provider.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/storybook/stories/input/searchable-dropdown-provider.ts b/storybook/stories/input/searchable-dropdown-provider.ts index 798df4d83..a19bc0350 100644 --- a/storybook/stories/input/searchable-dropdown-provider.ts +++ b/storybook/stories/input/searchable-dropdown-provider.ts @@ -53,7 +53,7 @@ const resolver: SearchableDropdownResolver = { graphicType: 'inline-svg', meta: '', }), - item({ title: 'Context 1', graphic: 'list' }), + item({ title: 'Context 1', graphic: 'list', meta: 'alarm' }), item({ title: 'Context 2', graphic: 'list', isDisabled: true }), item({ title: 'Context 3', subTitle: 'sub title 3', graphic: 'list' }), item({ title: 'Context 4', subTitle: 'sub title 4', graphic: 'list', isError: true }), @@ -73,6 +73,6 @@ const resolver: SearchableDropdownResolver = { ], }; -export const searchableDropdownProviderDecorator = (story) => { - return html` ${story()} `; +export const searchableDropdownProviderDecorator = (story: CallableFunction) => { + return html`${story()} `; }; From 4d49384ca5aa87c90ab56bf922e8b38d65a0fd46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Thu, 23 Jan 2025 12:41:53 +0100 Subject: [PATCH 13/18] chore: setting sdd as major release --- .changeset/yellow-carrots-play.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/yellow-carrots-play.md b/.changeset/yellow-carrots-play.md index f7ad09453..c71b74f81 100644 --- a/.changeset/yellow-carrots-play.md +++ b/.changeset/yellow-carrots-play.md @@ -1,5 +1,5 @@ --- -'@equinor/fusion-wc-searchable-dropdown': patch +'@equinor/fusion-wc-searchable-dropdown': major '@equinor/fusion-wc-storybook': patch --- From c8b2581882498ccc1be001d78a73268dfab9de43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Thu, 23 Jan 2025 12:53:35 +0100 Subject: [PATCH 14/18] fix(sdd): removed dependency for material types --- packages/searchable-dropdown/package.json | 2 -- packages/searchable-dropdown/src/types.ts | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/searchable-dropdown/package.json b/packages/searchable-dropdown/package.json index b1312b508..4cdc477bf 100644 --- a/packages/searchable-dropdown/package.json +++ b/packages/searchable-dropdown/package.json @@ -20,7 +20,6 @@ "@equinor/fusion-wc-textinput": "workspace:^", "@equinor/fusion-web-theme": "^0.1.10", "@lit-labs/task": "^3.1.0", - "@material/mwc-textfield": "^0.27.0", "@types/uuid": "^10.0.0", "lit": "3.2.1", "uuid": "^10.0.0" @@ -28,7 +27,6 @@ "devDependencies": { "@custom-elements-manifest/analyzer": "^0.10.4", "@equinor/fusion-wc-builder": "workspace:^", - "@material/mwc-list": "^0.27.0", "tslib": "^2.8.1", "typescript": "^5.6.2" }, diff --git a/packages/searchable-dropdown/src/types.ts b/packages/searchable-dropdown/src/types.ts index 2a631762f..52af6d074 100644 --- a/packages/searchable-dropdown/src/types.ts +++ b/packages/searchable-dropdown/src/types.ts @@ -1,5 +1,4 @@ import { ReactiveControllerHost } from 'lit'; -import { type ActionDetail } from '@material/mwc-list/mwc-list-foundation'; import { TextInputElement } from '@equinor/fusion-wc-textinput'; import { ListElement } from '@equinor/fusion-wc-list'; import { IconElement, IconType } from '@equinor/fusion-wc-icon'; @@ -95,7 +94,9 @@ export interface SearchableDropdownControllerHost extends SearchableDropdownProp } export interface ExplicitEventTarget extends Event { - readonly detail: ActionDetail; + readonly detail: { + index: number; + }; readonly explicitOriginalTarget: HTMLInputElement; } From 048a5c910cede52216586aa6cfd533a49f5079a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Thu, 23 Jan 2025 12:54:32 +0100 Subject: [PATCH 15/18] chore(package): lockfile --- pnpm-lock.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ecd025a2e..f843a3ebe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -481,9 +481,6 @@ importers: '@equinor/fusion-wc-list': specifier: workspace:^ version: link:../list - '@equinor/fusion-wc-searchable-dropdown': - specifier: workspace:^ - version: link:../searchable-dropdown '@equinor/fusion-wc-skeleton': specifier: workspace:^ version: link:../skeleton @@ -670,9 +667,6 @@ importers: '@lit-labs/task': specifier: ^3.1.0 version: 3.1.0 - '@material/mwc-textfield': - specifier: ^0.27.0 - version: 0.27.0 '@types/uuid': specifier: ^10.0.0 version: 10.0.0 @@ -689,9 +683,6 @@ importers: '@equinor/fusion-wc-builder': specifier: workspace:^ version: link:../../utils/builder - '@material/mwc-list': - specifier: ^0.27.0 - version: 0.27.0 tslib: specifier: ^2.8.1 version: 2.8.1 From 6cc4ed90e414eb60c5a610470533ad4938ab9532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Thu, 23 Jan 2025 13:05:43 +0100 Subject: [PATCH 16/18] fix(sdd): meta icon type --- packages/searchable-dropdown/src/dropdown/element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/searchable-dropdown/src/dropdown/element.ts b/packages/searchable-dropdown/src/dropdown/element.ts index 7953d4853..14b7bca95 100644 --- a/packages/searchable-dropdown/src/dropdown/element.ts +++ b/packages/searchable-dropdown/src/dropdown/element.ts @@ -227,7 +227,7 @@ export class SearchableDropdownElement return unsafeSVG(meta); default: if (meta) { - return html``; + return html``; } } } From 41f794d1da5052074dfd2c76b9c2f52cf71d7d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Thu, 23 Jan 2025 14:30:43 +0100 Subject: [PATCH 17/18] fix(sdd): remove space and person-select story example --- packages/searchable-dropdown/src/dropdown/element.ts | 2 +- storybook/stories/person/person-select.stories.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/searchable-dropdown/src/dropdown/element.ts b/packages/searchable-dropdown/src/dropdown/element.ts index 14b7bca95..f027cc7ca 100644 --- a/packages/searchable-dropdown/src/dropdown/element.ts +++ b/packages/searchable-dropdown/src/dropdown/element.ts @@ -334,7 +334,7 @@ export class SearchableDropdownElement @input=${this.controller.handleKeyup} > - ${this.renderCloseIcon()} + ${this.renderCloseIcon()}
diff --git a/storybook/stories/person/person-select.stories.ts b/storybook/stories/person/person-select.stories.ts index c7e356ac4..3cff5c0af 100644 --- a/storybook/stories/person/person-select.stories.ts +++ b/storybook/stories/person/person-select.stories.ts @@ -73,7 +73,8 @@ export const SetSelectedPersonAttributeWithAzureId: Story = { export const SetSelectedPersonAttributeWithPersonInfo: Story = { args: { // selectedPerson: JSON.stringify({ azureID: '49132c24-6ea4-41fe-8221-112f314573f0' }), - selectedPerson: '{"azureId":"49132c24-6ea4-41fe-8221-112f314573f0"}', + selectedPerson: + '{"azureId": "b4f6b901-902e-486a-979e-86d8eeee6365", "upn": "Noe.Rice@equinor.com", "name": "Naomi Steuber", "accountType": "Consultant", "accountClassification": "External", "jobTitle": "Product Assurance Representative", "department": "Automotive", "mail": "Noe.Rice@equinor.com", "mobilePhone": "(763) 255-9520", "officeLocation": "Catherinefield", "positions": [], "manager": { "azureUniqueId": "8da7cab8-5c43-4c99-9689-8c22501f6071","name": "Allan Kulas","department": "Health"}', }, render, }; From 0421e44dc0f26f1a0a0543f23efc4aa370a09e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Eikeland?= Date: Thu, 23 Jan 2025 16:06:30 +0100 Subject: [PATCH 18/18] feat(sdd): adding metatType for meta icon in result --- packages/searchable-dropdown/src/dropdown/element.css.ts | 3 +++ packages/searchable-dropdown/src/dropdown/element.ts | 8 ++++---- packages/searchable-dropdown/src/types.ts | 1 + storybook/stories/input/searchable-dropdown-provider.ts | 4 ++++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/searchable-dropdown/src/dropdown/element.css.ts b/packages/searchable-dropdown/src/dropdown/element.css.ts index c1f9d9ae4..4f12b2b4e 100644 --- a/packages/searchable-dropdown/src/dropdown/element.css.ts +++ b/packages/searchable-dropdown/src/dropdown/element.css.ts @@ -112,6 +112,9 @@ export const fwcsdd: CSSResult = css` height: 1em; width: auto; } + [slot='meta'] { + display: flex; + } fwc-divider { display: flex; align-items: center; diff --git a/packages/searchable-dropdown/src/dropdown/element.ts b/packages/searchable-dropdown/src/dropdown/element.ts index f027cc7ca..c62baabb9 100644 --- a/packages/searchable-dropdown/src/dropdown/element.ts +++ b/packages/searchable-dropdown/src/dropdown/element.ts @@ -200,7 +200,7 @@ export class SearchableDropdownElement >
${this.renderItemGraphic(item)}
${text} -
${this.renderItemMeta(item)}
+
${this.renderItemMeta(item)}
`; } @@ -219,15 +219,15 @@ export class SearchableDropdownElement } protected renderItemMeta(item: SearchableDropdownResultItem): ReturnType | void { - const { meta, graphicType } = item; - switch (graphicType) { + const { meta, metaType } = item; + switch (metaType) { case 'inline-html': return unsafeHTML(meta); case 'inline-svg': return unsafeSVG(meta); default: if (meta) { - return html``; + return html``; } } } diff --git a/packages/searchable-dropdown/src/types.ts b/packages/searchable-dropdown/src/types.ts index 52af6d074..fcd4d6991 100644 --- a/packages/searchable-dropdown/src/types.ts +++ b/packages/searchable-dropdown/src/types.ts @@ -65,6 +65,7 @@ export interface SearchableDropdownResultItem { isError?: boolean; isSelected?: boolean; meta?: string; + metaType?: IconType | 'inline-svg' | 'inline-html'; subTitle?: string; title?: string; type?: 'section' | 'divider' | null; diff --git a/storybook/stories/input/searchable-dropdown-provider.ts b/storybook/stories/input/searchable-dropdown-provider.ts index a19bc0350..d63f29bf0 100644 --- a/storybook/stories/input/searchable-dropdown-provider.ts +++ b/storybook/stories/input/searchable-dropdown-provider.ts @@ -7,6 +7,9 @@ import { faker } from '@faker-js/faker'; import { html } from 'lit'; import appIconSvgTemplate from './appIconSvg.svg'; +import { ChipElement } from '@equinor/fusion-wc-chip'; +ChipElement; + SearchableDropdownProviderElement; faker.seed(123); @@ -52,6 +55,7 @@ const resolver: SearchableDropdownResolver = { graphic: `
${appIconSvgTemplate}
`, graphicType: 'inline-svg', meta: '', + metaType: 'inline-html', }), item({ title: 'Context 1', graphic: 'list', meta: 'alarm' }), item({ title: 'Context 2', graphic: 'list', isDisabled: true }),