From 5e6dcc668a5ba14dca3ed3627d0af8eff322b16e Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 21 Nov 2022 12:40:27 +0200 Subject: [PATCH 01/82] scaffold basic component --- src/components/combo/combo.spec.ts | 0 src/components/combo/combo.ts | 66 +++++++++++++++++++ .../combo/themes/light/combo.base.scss | 6 ++ .../combo/themes/light/combo.bootstrap.scss | 0 .../combo/themes/light/combo.fluent.scss | 0 .../combo/themes/light/combo.indigo.scss | 0 .../combo/themes/light/combo.material.scss | 0 .../common/definitions/defineAllComponents.ts | 2 + src/index.ts | 1 + stories/combo.stories.ts | 41 ++++++++++++ 10 files changed, 116 insertions(+) create mode 100644 src/components/combo/combo.spec.ts create mode 100644 src/components/combo/combo.ts create mode 100644 src/components/combo/themes/light/combo.base.scss create mode 100644 src/components/combo/themes/light/combo.bootstrap.scss create mode 100644 src/components/combo/themes/light/combo.fluent.scss create mode 100644 src/components/combo/themes/light/combo.indigo.scss create mode 100644 src/components/combo/themes/light/combo.material.scss create mode 100644 stories/combo.stories.ts diff --git a/src/components/combo/combo.spec.ts b/src/components/combo/combo.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts new file mode 100644 index 000000000..adfef28fb --- /dev/null +++ b/src/components/combo/combo.ts @@ -0,0 +1,66 @@ +import { html, LitElement } from 'lit'; +import { themes } from '../../theming/theming-decorator.js'; +import { styles } from './themes/light/combo.base.css.js'; +import { styles as bootstrap } from './themes/light/combo.bootstrap.css.js'; +import { styles as material } from './themes/light/combo.material.css.js'; +import { styles as fluent } from './themes/light/combo.fluent.css.js'; +import { styles as indigo } from './themes/light/combo.indigo.css.js'; +import { property } from 'lit/decorators.js'; + +/** + * @element igc-combo + * + * @slot prefix - Renders content before the input. + * @slot suffix - Renders content after input. + * @slot header - Renders a container before the list of options. + * @slot footer - Renders a container after the list of options. + * @slot helper-text - Renders content below the input. + * @slot toggle-icon - Renders content inside the suffix container. + * @slot clear-icon - Renders content inside the suffix container. + * + * @fires igcFocus - Emitted when the select gains focus. + * @fires igcBlur - Emitted when the select loses focus. + * @fires igcChange - Emitted when the control's checked state changes. + * @fires igcOpening - Emitted just before the list of options is opened. + * @fires igcOpened - Emitted after the list of options is opened. + * @fires igcClosing - Emitter just before the list of options is closed. + * @fires igcClosed - Emitted after the list of options is closed. + * + * @csspart list - The list of options wrapper. + * @csspart input - The encapsulated igc-input. + * @csspart label - The encapsulated text label. + * @csspart prefix - The prefix wrapper. + * @csspart suffix - The suffix wrapper. + * @csspart toggle-icon - The toggle icon wrapper. + * @csspart helper-text - The helper text wrapper. + */ +@themes({ material, bootstrap, fluent, indigo }) +export default class IgcComboComponent extends LitElement { + public static readonly tagName = 'igc-combo'; + public static override styles = styles; + + /** The value attribute of the control. */ + @property({ reflect: false, type: String }) + public value?: string | undefined; + + /** The name attribute of the control. */ + @property() + public name!: string; + + /** The data source used to build the list of options. */ + @property({ attribute: false }) + public data: Array = []; + + public override render() { + return html` +
this is a combo component
+ + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'igc-combo': IgcComboComponent; + } +} diff --git a/src/components/combo/themes/light/combo.base.scss b/src/components/combo/themes/light/combo.base.scss new file mode 100644 index 000000000..b397492f6 --- /dev/null +++ b/src/components/combo/themes/light/combo.base.scss @@ -0,0 +1,6 @@ +@use '../../../../styles/common/component'; +@use '../../../../styles/utilities' as *; + +:host { + color: red; +} diff --git a/src/components/combo/themes/light/combo.bootstrap.scss b/src/components/combo/themes/light/combo.bootstrap.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/combo/themes/light/combo.fluent.scss b/src/components/combo/themes/light/combo.fluent.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/combo/themes/light/combo.indigo.scss b/src/components/combo/themes/light/combo.indigo.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/combo/themes/light/combo.material.scss b/src/components/combo/themes/light/combo.material.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/common/definitions/defineAllComponents.ts b/src/components/common/definitions/defineAllComponents.ts index c6634000d..fc784e506 100644 --- a/src/components/common/definitions/defineAllComponents.ts +++ b/src/components/common/definitions/defineAllComponents.ts @@ -12,6 +12,7 @@ import IgcCardMediaComponent from '../../card/card.media.js'; import IgcCheckboxComponent from '../../checkbox/checkbox.js'; import IgcChipComponent from '../../chip/chip.js'; import IgcCircularProgressComponent from '../../progress/circular-progress.js'; +import IgcComboComponent from '../../combo/combo.js'; import IgcDropdownComponent from '../../dropdown/dropdown.js'; import IgcDropdownGroupComponent from '../../dropdown/dropdown-group.js'; import IgcDropdownHeaderComponent from '../../dropdown/dropdown-header.js'; @@ -63,6 +64,7 @@ const allComponents: CustomElementConstructor[] = [ IgcCardComponent, IgcCheckboxComponent, IgcChipComponent, + IgcComboComponent, IgcDropdownComponent, IgcDropdownGroupComponent, IgcDropdownHeaderComponent, diff --git a/src/index.ts b/src/index.ts index d16e242f1..4fb83fdde 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ export { default as IgcCheckboxComponent } from './components/checkbox/checkbox. export { default as IgcCircularProgressComponent } from './components/progress/circular-progress.js'; export { default as IgcCircularGradientComponent } from './components/progress/circular-gradient.js'; export { default as IgcChipComponent } from './components/chip/chip.js'; +export { default as IgcComboComponent } from './components/combo/combo.js'; export { default as IgcDateTimeInputComponent } from './components/date-time-input/date-time-input.js'; export { default as IgcDialogComponent } from './components/dialog/dialog.js'; export { default as IgcDropdownComponent } from './components/dropdown/dropdown.js'; diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts new file mode 100644 index 000000000..d907fa5f7 --- /dev/null +++ b/stories/combo.stories.ts @@ -0,0 +1,41 @@ +import { html } from 'lit'; +import { Context, Story } from './story.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { defineAllComponents } from '../src/index.js'; + +defineAllComponents(); + +// region default +const metadata = { + title: 'Combo', + component: 'igc-combo', + argTypes: { + value: { + type: 'string | undefined', + description: 'The value attribute of the control.', + control: 'text', + }, + name: { + type: 'string', + description: 'The name attribute of the control.', + control: 'text', + }, + }, +}; +export default metadata; +interface ArgTypes { + value: string | undefined; + name: string; +} +// endregion + +const Template: Story = ( + { name }: ArgTypes, + { globals: { direction } }: Context +) => html` + + test default slot + +`; + +export const Basic = Template.bind({}); From d04e38e4e7b3d0313fb0794b96304e0b74016d38 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 21 Nov 2022 12:53:24 +0200 Subject: [PATCH 02/82] npm(lit-vitrualizer): virtualizer for combo list --- package-lock.json | 86 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 87 insertions(+) diff --git a/package-lock.json b/package-lock.json index 1c869f644..3050fa3a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "keep-a-changelog": "^2.1.0", "lint-staged": "^13.0.3", "lit-analyzer": "^2.0.0-pre.2", + "lit-virtualizer": "^0.4.2", "madge": "^5.0.1", "node-watch": "^0.7.3", "postcss": "^8.4.18", @@ -7190,6 +7191,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -9873,6 +9883,40 @@ "@types/trusted-types": "^2.0.2" } }, + "node_modules/lit-virtualizer": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lit-virtualizer/-/lit-virtualizer-0.4.2.tgz", + "integrity": "sha512-aytx/Qym8h7eIh3u17oT2FfgmhOixnk4IuJAOMIbA6E8szkbpnKUDSLDWlN9ihQyCb0eijV213P+4mlekOWKxA==", + "dev": true, + "dependencies": { + "event-target-shim": "^5.0.1", + "lit-element": "^2.0.0", + "lit-html": "^1.0.0", + "resize-observer-polyfill": "^1.5.1", + "tslib": "^1.10.0" + } + }, + "node_modules/lit-virtualizer/node_modules/lit-element": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.5.1.tgz", + "integrity": "sha512-ogu7PiJTA33bEK0xGu1dmaX5vhcRjBXCFexPja0e7P7jqLhTpNKYRPmE+GmiCaRVAbiQKGkUgkh/i6+bh++dPQ==", + "dev": true, + "dependencies": { + "lit-html": "^1.1.1" + } + }, + "node_modules/lit-virtualizer/node_modules/lit-html": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.4.1.tgz", + "integrity": "sha512-B9btcSgPYb1q4oSOb/PrOT6Z/H+r6xuNzfH4lFli/AWhYwdtrgQkQWBbIc6mdnf6E2IL3gDXdkkqNktpU0OZQA==", + "dev": true + }, + "node_modules/lit-virtualizer/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/loader-utils": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", @@ -22421,6 +22465,12 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true + }, "eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -24418,6 +24468,42 @@ "@types/trusted-types": "^2.0.2" } }, + "lit-virtualizer": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lit-virtualizer/-/lit-virtualizer-0.4.2.tgz", + "integrity": "sha512-aytx/Qym8h7eIh3u17oT2FfgmhOixnk4IuJAOMIbA6E8szkbpnKUDSLDWlN9ihQyCb0eijV213P+4mlekOWKxA==", + "dev": true, + "requires": { + "event-target-shim": "^5.0.1", + "lit-element": "^2.0.0", + "lit-html": "^1.0.0", + "resize-observer-polyfill": "^1.5.1", + "tslib": "^1.10.0" + }, + "dependencies": { + "lit-element": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.5.1.tgz", + "integrity": "sha512-ogu7PiJTA33bEK0xGu1dmaX5vhcRjBXCFexPja0e7P7jqLhTpNKYRPmE+GmiCaRVAbiQKGkUgkh/i6+bh++dPQ==", + "dev": true, + "requires": { + "lit-html": "^1.1.1" + } + }, + "lit-html": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.4.1.tgz", + "integrity": "sha512-B9btcSgPYb1q4oSOb/PrOT6Z/H+r6xuNzfH4lFli/AWhYwdtrgQkQWBbIc6mdnf6E2IL3gDXdkkqNktpU0OZQA==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, "loader-utils": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", diff --git a/package.json b/package.json index 2e75482c6..7127acb99 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "keep-a-changelog": "^2.1.0", "lint-staged": "^13.0.3", "lit-analyzer": "^2.0.0-pre.2", + "lit-virtualizer": "^0.4.2", "madge": "^5.0.1", "node-watch": "^0.7.3", "postcss": "^8.4.18", From 114ad85142ec47601686ef6865f301ae11c5a873 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 21 Nov 2022 18:31:33 +0200 Subject: [PATCH 03/82] feat(combo): basic item implementation --- package-lock.json | 128 +++++++----------- package.json | 3 +- src/components/combo/combo-item.ts | 60 ++++++++ src/components/combo/combo.ts | 57 +++++++- .../themes/light/item/combo-item.base.scss | 6 + .../common/definitions/defineAllComponents.ts | 2 + src/index.ts | 1 + stories/combo.stories.ts | 50 ++++++- 8 files changed, 218 insertions(+), 89 deletions(-) create mode 100644 src/components/combo/combo-item.ts create mode 100644 src/components/combo/themes/light/item/combo-item.base.scss diff --git a/package-lock.json b/package-lock.json index 3050fa3a7..ff9f57139 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,9 @@ "lit": "^2.4.0" }, "devDependencies": { + "@faker-js/faker": "^7.6.0", "@igniteui/material-icons-extended": "^2.11.0", + "@lit-labs/virtualizer": "^0.7.2", "@open-wc/eslint-config": "^8.0.2", "@open-wc/testing": "^3.1.6", "@storybook/storybook-deployer": "^2.8.16", @@ -35,7 +37,6 @@ "keep-a-changelog": "^2.1.0", "lint-staged": "^13.0.3", "lit-analyzer": "^2.0.0-pre.2", - "lit-virtualizer": "^0.4.2", "madge": "^5.0.1", "node-watch": "^0.7.3", "postcss": "^8.4.18", @@ -1781,6 +1782,16 @@ "@types/chai": "^4.2.12" } }, + "node_modules/@faker-js/faker": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", + "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "dev": true, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.1.tgz", @@ -1911,6 +1922,17 @@ "lit": "^2.0.2" } }, + "node_modules/@lit-labs/virtualizer": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@lit-labs/virtualizer/-/virtualizer-0.7.2.tgz", + "integrity": "sha512-P6sUc9ixaIGYU7gOV3O4ZGrO1Eo1c1sVYGwTIJdJTxXzobi4e5Di6beKksv9HzZdHkTj1JHapCj9hRcSO5kmMQ==", + "dev": true, + "dependencies": { + "event-target-shim": "^6.0.2", + "lit": "^2.0.0", + "tslib": "^2.0.3" + } + }, "node_modules/@lit/reactive-element": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.4.0.tgz", @@ -7192,12 +7214,15 @@ } }, "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz", + "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==", "dev": true, "engines": { - "node": ">=6" + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" } }, "node_modules/eventemitter3": { @@ -9883,40 +9908,6 @@ "@types/trusted-types": "^2.0.2" } }, - "node_modules/lit-virtualizer": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/lit-virtualizer/-/lit-virtualizer-0.4.2.tgz", - "integrity": "sha512-aytx/Qym8h7eIh3u17oT2FfgmhOixnk4IuJAOMIbA6E8szkbpnKUDSLDWlN9ihQyCb0eijV213P+4mlekOWKxA==", - "dev": true, - "dependencies": { - "event-target-shim": "^5.0.1", - "lit-element": "^2.0.0", - "lit-html": "^1.0.0", - "resize-observer-polyfill": "^1.5.1", - "tslib": "^1.10.0" - } - }, - "node_modules/lit-virtualizer/node_modules/lit-element": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.5.1.tgz", - "integrity": "sha512-ogu7PiJTA33bEK0xGu1dmaX5vhcRjBXCFexPja0e7P7jqLhTpNKYRPmE+GmiCaRVAbiQKGkUgkh/i6+bh++dPQ==", - "dev": true, - "dependencies": { - "lit-html": "^1.1.1" - } - }, - "node_modules/lit-virtualizer/node_modules/lit-html": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.4.1.tgz", - "integrity": "sha512-B9btcSgPYb1q4oSOb/PrOT6Z/H+r6xuNzfH4lFli/AWhYwdtrgQkQWBbIc6mdnf6E2IL3gDXdkkqNktpU0OZQA==", - "dev": true - }, - "node_modules/lit-virtualizer/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/loader-utils": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", @@ -18295,6 +18286,12 @@ "@types/chai": "^4.2.12" } }, + "@faker-js/faker": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", + "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "dev": true + }, "@floating-ui/core": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.1.tgz", @@ -18406,6 +18403,17 @@ "lit": "^2.0.2" } }, + "@lit-labs/virtualizer": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@lit-labs/virtualizer/-/virtualizer-0.7.2.tgz", + "integrity": "sha512-P6sUc9ixaIGYU7gOV3O4ZGrO1Eo1c1sVYGwTIJdJTxXzobi4e5Di6beKksv9HzZdHkTj1JHapCj9hRcSO5kmMQ==", + "dev": true, + "requires": { + "event-target-shim": "^6.0.2", + "lit": "^2.0.0", + "tslib": "^2.0.3" + } + }, "@lit/reactive-element": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.4.0.tgz", @@ -22466,9 +22474,9 @@ "dev": true }, "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz", + "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==", "dev": true }, "eventemitter3": { @@ -24468,42 +24476,6 @@ "@types/trusted-types": "^2.0.2" } }, - "lit-virtualizer": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/lit-virtualizer/-/lit-virtualizer-0.4.2.tgz", - "integrity": "sha512-aytx/Qym8h7eIh3u17oT2FfgmhOixnk4IuJAOMIbA6E8szkbpnKUDSLDWlN9ihQyCb0eijV213P+4mlekOWKxA==", - "dev": true, - "requires": { - "event-target-shim": "^5.0.1", - "lit-element": "^2.0.0", - "lit-html": "^1.0.0", - "resize-observer-polyfill": "^1.5.1", - "tslib": "^1.10.0" - }, - "dependencies": { - "lit-element": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.5.1.tgz", - "integrity": "sha512-ogu7PiJTA33bEK0xGu1dmaX5vhcRjBXCFexPja0e7P7jqLhTpNKYRPmE+GmiCaRVAbiQKGkUgkh/i6+bh++dPQ==", - "dev": true, - "requires": { - "lit-html": "^1.1.1" - } - }, - "lit-html": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.4.1.tgz", - "integrity": "sha512-B9btcSgPYb1q4oSOb/PrOT6Z/H+r6xuNzfH4lFli/AWhYwdtrgQkQWBbIc6mdnf6E2IL3gDXdkkqNktpU0OZQA==", - "dev": true - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, "loader-utils": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", diff --git a/package.json b/package.json index 7127acb99..5c11f8943 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,9 @@ "lit": "^2.4.0" }, "devDependencies": { + "@faker-js/faker": "^7.6.0", "@igniteui/material-icons-extended": "^2.11.0", + "@lit-labs/virtualizer": "^0.7.2", "@open-wc/eslint-config": "^8.0.2", "@open-wc/testing": "^3.1.6", "@storybook/storybook-deployer": "^2.8.16", @@ -79,7 +81,6 @@ "keep-a-changelog": "^2.1.0", "lint-staged": "^13.0.3", "lit-analyzer": "^2.0.0-pre.2", - "lit-virtualizer": "^0.4.2", "madge": "^5.0.1", "node-watch": "^0.7.3", "postcss": "^8.4.18", diff --git a/src/components/combo/combo-item.ts b/src/components/combo/combo-item.ts new file mode 100644 index 000000000..57c846dc3 --- /dev/null +++ b/src/components/combo/combo-item.ts @@ -0,0 +1,60 @@ +import { html, LitElement } from 'lit'; +import { themes } from '../../theming/theming-decorator.js'; +import { styles } from './themes/light/item/combo-item.base.css'; +import { styles as bootstrap } from '../dropdown/themes/light/dropdown-item.bootstrap.css'; +import { styles as fluent } from '../dropdown/themes/light/dropdown-item.fluent.css'; +import { styles as indigo } from '../dropdown/themes/light/dropdown-item.indigo.css'; +import { property } from 'lit/decorators.js'; +import { watch } from '../common/decorators/watch.js'; +import IgcCheckboxComopnent from '../checkbox/checkbox.js'; +import { defineComponents } from '../common/definitions/defineComponents.js'; + +defineComponents(IgcCheckboxComopnent); + +@themes({ bootstrap, fluent, indigo }) +export default class IgcComboItemComponent extends LitElement { + public static readonly tagName: string = 'igc-combo-item'; + public static override styles = styles; + + /** + * Determines whether the item is selected. + */ + @property({ type: Boolean, reflect: true }) + public selected = false; + + /** + * Determines whether the item is active. + */ + @property({ type: Boolean, reflect: true }) + public active = false; + + @watch('selected') + protected selectedChange() { + this.selected + ? this.setAttribute('aria-selected', 'true') + : this.removeAttribute('aria-selected'); + this.active = this.selected; + } + + public override connectedCallback() { + super.connectedCallback(); + this.setAttribute('role', 'option'); + } + + protected override render() { + return html` +
+ +
+
+ +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'igc-combo-item': IgcComboItemComponent; + } +} diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index adfef28fb..e76e07b68 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -1,11 +1,17 @@ -import { html, LitElement } from 'lit'; +import { html, LitElement, TemplateResult } from 'lit'; import { themes } from '../../theming/theming-decorator.js'; import { styles } from './themes/light/combo.base.css.js'; import { styles as bootstrap } from './themes/light/combo.bootstrap.css.js'; import { styles as material } from './themes/light/combo.material.css.js'; import { styles as fluent } from './themes/light/combo.fluent.css.js'; import { styles as indigo } from './themes/light/combo.indigo.css.js'; -import { property } from 'lit/decorators.js'; +import { property, state } from 'lit/decorators.js'; +import { virtualize } from '@lit-labs/virtualizer/virtualize.js'; +import { watch } from '../common/decorators/watch.js'; +import { defineComponents } from '../common/definitions/defineComponents.js'; +import IgcComboItemComponent from './combo-item.js'; + +defineComponents(IgcComboItemComponent); /** * @element igc-combo @@ -35,10 +41,16 @@ import { property } from 'lit/decorators.js'; * @csspart helper-text - The helper text wrapper. */ @themes({ material, bootstrap, fluent, indigo }) -export default class IgcComboComponent extends LitElement { +export default class IgcComboComponent extends LitElement { public static readonly tagName = 'igc-combo'; public static override styles = styles; + @property({ attribute: 'value-key' }) + public valueKey?: keyof T; + + @property({ attribute: 'display-key' }) + public displayKey?: keyof T = this.valueKey; + /** The value attribute of the control. */ @property({ reflect: false, type: String }) public value?: string | undefined; @@ -49,18 +61,49 @@ export default class IgcComboComponent extends LitElement { /** The data source used to build the list of options. */ @property({ attribute: false }) - public data: Array = []; + public data: Array = []; + + @state() + protected dataState: Array = []; + + @watch('data') + protected dataChanged() { + this.dataState = structuredClone(this.data); + } + + @watch('valueKey') + protected updateDisplayKey() { + this.displayKey = this.displayKey ?? this.valueKey; + } + + @property({ attribute: false }) + public itemTemplate: (item: T) => TemplateResult = (item) => { + if (this.displayKey) { + return html`${(item as any)[this.displayKey]}`; + } + + return html`${item}`; + }; + + protected itemRenderer = (item: T): TemplateResult => { + return html`${this.itemTemplate(item)}`; + }; public override render() { return html` -
this is a combo component
- +
+ ${virtualize({ + scroller: true, + items: this.dataState, + renderItem: this.itemRenderer, + })} +
`; } } declare global { interface HTMLElementTagNameMap { - 'igc-combo': IgcComboComponent; + 'igc-combo': IgcComboComponent; } } diff --git a/src/components/combo/themes/light/item/combo-item.base.scss b/src/components/combo/themes/light/item/combo-item.base.scss new file mode 100644 index 000000000..9238677e4 --- /dev/null +++ b/src/components/combo/themes/light/item/combo-item.base.scss @@ -0,0 +1,6 @@ +@use '../../../../../styles/utilities' as *; +@use '../../../../dropdown/themes/light/dropdown-item.base' as *; + +:host { + gap: rem(8px); +} diff --git a/src/components/common/definitions/defineAllComponents.ts b/src/components/common/definitions/defineAllComponents.ts index fc784e506..b078c94ca 100644 --- a/src/components/common/definitions/defineAllComponents.ts +++ b/src/components/common/definitions/defineAllComponents.ts @@ -13,6 +13,7 @@ import IgcCheckboxComponent from '../../checkbox/checkbox.js'; import IgcChipComponent from '../../chip/chip.js'; import IgcCircularProgressComponent from '../../progress/circular-progress.js'; import IgcComboComponent from '../../combo/combo.js'; +import IgcComboItemComponent from '../../combo/combo-item.js'; import IgcDropdownComponent from '../../dropdown/dropdown.js'; import IgcDropdownGroupComponent from '../../dropdown/dropdown-group.js'; import IgcDropdownHeaderComponent from '../../dropdown/dropdown-header.js'; @@ -65,6 +66,7 @@ const allComponents: CustomElementConstructor[] = [ IgcCheckboxComponent, IgcChipComponent, IgcComboComponent, + IgcComboItemComponent, IgcDropdownComponent, IgcDropdownGroupComponent, IgcDropdownHeaderComponent, diff --git a/src/index.ts b/src/index.ts index 4fb83fdde..5f07af083 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ export { default as IgcCircularProgressComponent } from './components/progress/c export { default as IgcCircularGradientComponent } from './components/progress/circular-gradient.js'; export { default as IgcChipComponent } from './components/chip/chip.js'; export { default as IgcComboComponent } from './components/combo/combo.js'; +export { default as IgcComboItemComponent } from './components/combo/combo-item.js'; export { default as IgcDateTimeInputComponent } from './components/date-time-input/date-time-input.js'; export { default as IgcDialogComponent } from './components/dialog/dialog.js'; export { default as IgcDropdownComponent } from './components/dropdown/dropdown.js'; diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index d907fa5f7..8e47d020d 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -2,6 +2,7 @@ import { html } from 'lit'; import { Context, Story } from './story.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { defineAllComponents } from '../src/index.js'; +import { faker } from '@faker-js/faker'; defineAllComponents(); @@ -29,13 +30,56 @@ interface ArgTypes { } // endregion +interface City { + id: string; + name: string; + zip: string; + country: string; +} + +function generateCity(): City { + const id = faker.datatype.uuid(); + const name = faker.address.cityName(); + const zip = faker.address.zipCode(); + const country = faker.address.country(); + + return { + id, + name, + zip, + country, + }; +} + +function generateCities(amount = 200) { + const result: Array = []; + + for (let i = 0; i <= amount; i++) { + result.push(generateCity()); + } + + return result; +} + +// const itemTemplate = (item: City) => { +// return html` +//
+// ${item.name}, ${item.country} +//
+// `; +// }; + const Template: Story = ( { name }: ArgTypes, { globals: { direction } }: Context ) => html` - - test default slot - + `; export const Basic = Template.bind({}); From 8579ee3b3c5bacc10835da0bec5bdf6966eb84a1 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 23 Nov 2022 15:56:44 +0200 Subject: [PATCH 04/82] feat(navigation): add item navigation --- src/components/combo/combo-header.ts | 20 +++ src/components/combo/combo.ts | 92 ++++++++++++-- .../combo/controllers/navigation.ts | 116 ++++++++++++++++++ stories/combo.stories.ts | 14 +++ 4 files changed, 231 insertions(+), 11 deletions(-) create mode 100644 src/components/combo/combo-header.ts create mode 100644 src/components/combo/controllers/navigation.ts diff --git a/src/components/combo/combo-header.ts b/src/components/combo/combo-header.ts new file mode 100644 index 000000000..04cd7bded --- /dev/null +++ b/src/components/combo/combo-header.ts @@ -0,0 +1,20 @@ +import { html, LitElement } from 'lit'; +import { themes } from '../../theming/theming-decorator.js'; +import { styles } from '../dropdown/themes/light/dropdown-header.base.css.js'; +import { styles as bootstrap } from '../dropdown/themes/light/dropdown-header.bootstrap.css.js'; +import { styles as fluent } from '../dropdown/themes/light/dropdown-header.fluent.css.js'; + +@themes({ bootstrap, fluent }) +export default class IgcComboHeaderComponent extends LitElement { + public static readonly tagName: string = 'igc-combo-header'; + public static override styles = styles; + + protected override render() { + return html``; + } +} +declare global { + interface HTMLElementTagNameMap { + 'igc-combo-header': IgcComboHeaderComponent; + } +} diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index e76e07b68..8b03c90c2 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -5,13 +5,15 @@ import { styles as bootstrap } from './themes/light/combo.bootstrap.css.js'; import { styles as material } from './themes/light/combo.material.css.js'; import { styles as fluent } from './themes/light/combo.fluent.css.js'; import { styles as indigo } from './themes/light/combo.indigo.css.js'; -import { property, state } from 'lit/decorators.js'; +import { property, queryAll, state } from 'lit/decorators.js'; import { virtualize } from '@lit-labs/virtualizer/virtualize.js'; import { watch } from '../common/decorators/watch.js'; import { defineComponents } from '../common/definitions/defineComponents.js'; import IgcComboItemComponent from './combo-item.js'; +import IgcComboHeaderComponent from './combo-header.js'; +import { NavigationController } from './controllers/navigation.js'; -defineComponents(IgcComboItemComponent); +defineComponents(IgcComboItemComponent, IgcComboHeaderComponent); /** * @element igc-combo @@ -45,12 +47,17 @@ export default class IgcComboComponent extends LitElement { public static readonly tagName = 'igc-combo'; public static override styles = styles; + protected navigationController = new NavigationController(this); + @property({ attribute: 'value-key' }) public valueKey?: keyof T; @property({ attribute: 'display-key' }) public displayKey?: keyof T = this.valueKey; + @property({ attribute: 'group-key' }) + public groupKey?: keyof T = this.displayKey; + /** The value attribute of the control. */ @property({ reflect: false, type: String }) public value?: string | undefined; @@ -63,8 +70,14 @@ export default class IgcComboComponent extends LitElement { @property({ attribute: false }) public data: Array = []; + @property() + public scrollIndex = 0; + + @queryAll('igc-combo-item') + public items!: NodeListOf; + @state() - protected dataState: Array = []; + public dataState: Array = []; @watch('data') protected dataChanged() { @@ -76,6 +89,31 @@ export default class IgcComboComponent extends LitElement { this.displayKey = this.displayKey ?? this.valueKey; } + @watch('groupKey') + protected groupItems() { + if (!this.groupKey) return; + + this.dataState = Object.values( + this.dataState.reduce((acc: any, obj: any) => { + const key = obj[this.groupKey]; + + if (!acc[key]) { + acc[key] = []; + acc[key].push({ + [this.valueKey as string]: key, + [this.displayKey as string]: key, + [this.groupKey as string]: key, + header: true, + }); + } + acc[key].push({ ...obj, header: false }); + return acc; + }, {}) + ); + + this.dataState = this.dataState.flat(); + } + @property({ attribute: false }) public itemTemplate: (item: T) => TemplateResult = (item) => { if (this.displayKey) { @@ -85,18 +123,50 @@ export default class IgcComboComponent extends LitElement { return html`${item}`; }; - protected itemRenderer = (item: T): TemplateResult => { - return html`${this.itemTemplate(item)}`; + @property({ attribute: false }) + public headerItemTemplate: (item: T) => TemplateResult = (item) => { + return html`${(item as any)[this.groupKey]}`; }; + protected itemRenderer = (item: T, index: number): TemplateResult => { + const headerTemplate = html`${this.headerItemTemplate(item)}`; + const itemTemplate = html`${this.itemTemplate(item)}`; + + return (item as any)?.header ? headerTemplate : itemTemplate; + }; + + public scrollToIndex(index: number) { + this.scrollIndex = index; + } + + // public get totalItems() { + // return this.dataState.filter((i) => i.header !== true).length; + // } + + protected keydownHandler(event: KeyboardEvent) { + this.navigationController.navigate(event); + } + public override render() { return html` -
- ${virtualize({ - scroller: true, - items: this.dataState, - renderItem: this.itemRenderer, - })} +
+
+ ${virtualize({ + scroller: true, + items: this.dataState, + renderItem: this.itemRenderer, + scrollToIndex: { + index: this.scrollIndex, + }, + })} +
`; } diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts new file mode 100644 index 000000000..5b9f08890 --- /dev/null +++ b/src/components/combo/controllers/navigation.ts @@ -0,0 +1,116 @@ +import { ReactiveController, ReactiveControllerHost } from 'lit'; +import IgcComboComponent from '../combo'; + +type ComboHost = ReactiveControllerHost & + IgcComboComponent; + +const START_INDEX: Readonly = -1; + +enum DIRECTION { + Up = -1, + Down = 1, +} + +export class NavigationController + implements ReactiveController +{ + protected handlers = new Map( + Object.entries({ + ArrowDown: this.arrowDown, + ArrowUp: this.arrowUp, + Home: this.home, + End: this.end, + }) + ); + + protected _active = START_INDEX; + + protected get currentNode() { + const node = this.active; + return node === START_INDEX ? 0 : node; + } + + protected get firstItemIndex() { + const items = this.host.dataState.filter((i) => (i as any).header !== true); + return this.host.dataState.indexOf(items[0]); + } + + protected get lastItemIndex() { + return this.host.dataState.length - 1; + } + + public get active() { + return this._active; + } + + public set active(node: any) { + this._active = node; + this.host.requestUpdate(); + } + + constructor(protected host: ComboHost) { + this.host.addController(this); + } + + protected home() { + this.active = this.firstItemIndex; + this.host.scrollToIndex(this.active); + } + + protected end() { + this.active = this.lastItemIndex; + this.host.scrollToIndex(this.active); + } + + protected arrowDown() { + this.navigateTo(DIRECTION.Down); + } + + protected arrowUp() { + this.navigateTo(DIRECTION.Up); + } + + protected navigateTo(direction: DIRECTION) { + const index = this.getNearestItem(this.currentNode, direction); + + if (index === -1) { + return; + } + + this.active = index; + this.host.scrollToIndex(index); + } + + protected getNearestItem(startIndex: number, direction: number) { + let index = startIndex; + const items = this.host.dataState; + + while ( + items[index + direction] && + (items[index + direction] as any).header + ) { + index += direction; + } + + index += direction; + + if (index >= 0 && index < items.length) { + return index; + } else { + return -1; + } + } + + public hostConnected() {} + + public hostDisconnected() { + this.active = START_INDEX; + } + + public navigate(event: KeyboardEvent) { + if (this.handlers.has(event.key)) { + event.preventDefault(); + this.handlers.get(event.key)!.call(this); + } + } +} diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 8e47d020d..804da8cef 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -21,12 +21,18 @@ const metadata = { description: 'The name attribute of the control.', control: 'text', }, + scrollIndex: { + type: 'number', + control: 'number', + defaultValue: '0', + }, }, }; export default metadata; interface ArgTypes { value: string | undefined; name: string; + scrollIndex: number; } // endregion @@ -69,6 +75,13 @@ function generateCities(amount = 200) { // `; // }; +// const headerItemTemplate = (item: City) => { +// return html`Group header for ${item.country}`; +// }; + +// .itemTemplate=${itemTemplate} +// .headerItemTemplate=${headerItemTemplate} + const Template: Story = ( { name }: ArgTypes, { globals: { direction } }: Context @@ -78,6 +91,7 @@ const Template: Story = ( dir=${ifDefined(direction)} value-key="id" display-key="name" + group-key="country" .data=${generateCities(1000)} > `; From 4944ed2651425a078abd4d5e8aac27ea0b38acdf Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 23 Nov 2022 17:07:20 +0200 Subject: [PATCH 05/82] refactor(combo): activate item on navigation --- src/components/combo/combo.ts | 15 +++++---------- stories/combo.stories.ts | 6 ------ 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 8b03c90c2..4ef130ed4 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -70,8 +70,8 @@ export default class IgcComboComponent extends LitElement { @property({ attribute: false }) public data: Array = []; - @property() - public scrollIndex = 0; + @state() + private scrollIndex = -1; @queryAll('igc-combo-item') public items!: NodeListOf; @@ -132,24 +132,19 @@ export default class IgcComboComponent extends LitElement { const headerTemplate = html`${this.headerItemTemplate(item)}`; + const itemTemplate = html`${this.itemTemplate(item)}`; - return (item as any)?.header ? headerTemplate : itemTemplate; + return html`${(item as any)?.header ? headerTemplate : itemTemplate}`; }; public scrollToIndex(index: number) { this.scrollIndex = index; } - // public get totalItems() { - // return this.dataState.filter((i) => i.header !== true).length; - // } - protected keydownHandler(event: KeyboardEvent) { this.navigationController.navigate(event); } diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 804da8cef..a87e880b0 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -21,18 +21,12 @@ const metadata = { description: 'The name attribute of the control.', control: 'text', }, - scrollIndex: { - type: 'number', - control: 'number', - defaultValue: '0', - }, }, }; export default metadata; interface ArgTypes { value: string | undefined; name: string; - scrollIndex: number; } // endregion From c0750e8ea27eb563e3fea1777af53c81f7ca4f92 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 23 Nov 2022 23:27:26 +0200 Subject: [PATCH 06/82] refactor(combo): update navigation --- src/components/combo/combo.ts | 12 ++--- .../combo/controllers/navigation.ts | 44 ++++++++----------- stories/combo.stories.ts | 2 +- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 4ef130ed4..7df0b00b3 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -49,6 +49,9 @@ export default class IgcComboComponent extends LitElement { protected navigationController = new NavigationController(this); + private scrollIndex = 0; + private scrollPosition = 'center'; + @property({ attribute: 'value-key' }) public valueKey?: keyof T; @@ -70,9 +73,6 @@ export default class IgcComboComponent extends LitElement { @property({ attribute: false }) public data: Array = []; - @state() - private scrollIndex = -1; - @queryAll('igc-combo-item') public items!: NodeListOf; @@ -106,7 +106,7 @@ export default class IgcComboComponent extends LitElement { header: true, }); } - acc[key].push({ ...obj, header: false }); + acc[key].push(Object.assign(obj, { header: false })); return acc; }, {}) ); @@ -141,8 +141,9 @@ export default class IgcComboComponent extends LitElement { return html`${(item as any)?.header ? headerTemplate : itemTemplate}`; }; - public scrollToIndex(index: number) { + public scrollToIndex(index: number, position?: string) { this.scrollIndex = index; + if (position) this.scrollPosition = position; } protected keydownHandler(event: KeyboardEvent) { @@ -159,6 +160,7 @@ export default class IgcComboComponent extends LitElement { renderItem: this.itemRenderer, scrollToIndex: { index: this.scrollIndex, + position: this.scrollPosition, }, })}
diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts index 5b9f08890..9d5643fd8 100644 --- a/src/components/combo/controllers/navigation.ts +++ b/src/components/combo/controllers/navigation.ts @@ -25,17 +25,16 @@ export class NavigationController protected _active = START_INDEX; - protected get currentNode() { - const node = this.active; - return node === START_INDEX ? 0 : node; + protected get currentItem() { + const item = this.active; + return item === START_INDEX ? START_INDEX : item; } - protected get firstItemIndex() { - const items = this.host.dataState.filter((i) => (i as any).header !== true); - return this.host.dataState.indexOf(items[0]); + protected get firstItem() { + return this.host.dataState.findIndex((i: any) => i.header !== true); } - protected get lastItemIndex() { + protected get lastItem() { return this.host.dataState.length - 1; } @@ -43,7 +42,7 @@ export class NavigationController return this._active; } - public set active(node: any) { + public set active(node: number) { this._active = node; this.host.requestUpdate(); } @@ -53,43 +52,38 @@ export class NavigationController } protected home() { - this.active = this.firstItemIndex; + this.active = this.firstItem; this.host.scrollToIndex(this.active); } - protected end() { - this.active = this.lastItemIndex; + protected async end() { + this.active = this.lastItem; this.host.scrollToIndex(this.active); } protected arrowDown() { - this.navigateTo(DIRECTION.Down); + this.getNextItem(DIRECTION.Down); } protected arrowUp() { - this.navigateTo(DIRECTION.Up); + this.getNextItem(DIRECTION.Up); } - protected navigateTo(direction: DIRECTION) { - const index = this.getNearestItem(this.currentNode, direction); + protected getNextItem(direction: DIRECTION) { + const next = this.getNearestItem(this.currentItem, direction); - if (index === -1) { - return; - } + if (next === -1) return; - this.active = index; - this.host.scrollToIndex(index); + this.active = next; + this.host.scrollToIndex(this.active); } protected getNearestItem(startIndex: number, direction: number) { let index = startIndex; const items = this.host.dataState; - while ( - items[index + direction] && - (items[index + direction] as any).header - ) { - index += direction; + if ((items[index + direction] as any)?.header) { + this.getNearestItem((index += direction), direction); } index += direction; diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index a87e880b0..48aa1a929 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -86,7 +86,7 @@ const Template: Story = ( value-key="id" display-key="name" group-key="country" - .data=${generateCities(1000)} + .data=${generateCities(200)} > `; From b70630536d2421581e9e5fcab13b6f0670bb402a Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Sat, 26 Nov 2022 16:04:48 +0200 Subject: [PATCH 07/82] refactor(combo): add typings and controllers --- src/components/combo/combo-item.ts | 8 ++ src/components/combo/combo.ts | 110 ++++++++++++------ src/components/combo/controllers/data.ts | 18 +++ .../combo/controllers/navigation.ts | 11 +- src/components/combo/operations/group.ts | 30 +++++ src/components/combo/types.ts | 14 +++ stories/combo.stories.ts | 54 ++++++++- stories/linear-progress.stories.ts | 6 +- stories/nav-drawer.stories.ts | 6 +- 9 files changed, 208 insertions(+), 49 deletions(-) create mode 100644 src/components/combo/controllers/data.ts create mode 100644 src/components/combo/operations/group.ts create mode 100644 src/components/combo/types.ts diff --git a/src/components/combo/combo-item.ts b/src/components/combo/combo-item.ts index 57c846dc3..daf4aa968 100644 --- a/src/components/combo/combo-item.ts +++ b/src/components/combo/combo-item.ts @@ -16,6 +16,12 @@ export default class IgcComboItemComponent extends LitElement { public static readonly tagName: string = 'igc-combo-item'; public static override styles = styles; + @property({ attribute: false }) + public activeNode!: number; + + @property({ attribute: false }) + public index!: number; + /** * Determines whether the item is selected. */ @@ -42,6 +48,8 @@ export default class IgcComboItemComponent extends LitElement { } protected override render() { + this.active = this.activeNode === this.index; + return html`
diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 7df0b00b3..6214c3920 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -5,13 +5,17 @@ import { styles as bootstrap } from './themes/light/combo.bootstrap.css.js'; import { styles as material } from './themes/light/combo.material.css.js'; import { styles as fluent } from './themes/light/combo.fluent.css.js'; import { styles as indigo } from './themes/light/combo.indigo.css.js'; -import { property, queryAll, state } from 'lit/decorators.js'; +import { property, query, queryAll, state } from 'lit/decorators.js'; import { virtualize } from '@lit-labs/virtualizer/virtualize.js'; import { watch } from '../common/decorators/watch.js'; import { defineComponents } from '../common/definitions/defineComponents.js'; import IgcComboItemComponent from './combo-item.js'; import IgcComboHeaderComponent from './combo-header.js'; import { NavigationController } from './controllers/navigation.js'; +import { IgcToggleController } from '../toggle/toggle.controller.js'; +import { IgcToggleComponent } from '../toggle/types.js'; +import { Keys, ComboRecord } from './types.js'; +import { DataController } from './controllers/data.js'; defineComponents(IgcComboItemComponent, IgcComboHeaderComponent); @@ -43,23 +47,38 @@ defineComponents(IgcComboItemComponent, IgcComboHeaderComponent); * @csspart helper-text - The helper text wrapper. */ @themes({ material, bootstrap, fluent, indigo }) -export default class IgcComboComponent extends LitElement { +export default class IgcComboComponent + extends LitElement + implements IgcToggleComponent +{ public static readonly tagName = 'igc-combo'; public static override styles = styles; protected navigationController = new NavigationController(this); + protected dataController = new DataController(this); + protected toggleController!: IgcToggleController; private scrollIndex = 0; private scrollPosition = 'center'; + @query('[part="target"]') + private target!: HTMLElement; + + @queryAll('igc-combo-item') + public items!: NodeListOf; + + /** Sets the open state of the component. */ + @property({ type: Boolean }) + public open = false; + @property({ attribute: 'value-key' }) - public valueKey?: keyof T; + public valueKey?: Keys; @property({ attribute: 'display-key' }) - public displayKey?: keyof T = this.valueKey; + public displayKey?: Keys = this.valueKey; @property({ attribute: 'group-key' }) - public groupKey?: keyof T = this.displayKey; + public groupKey?: Keys = this.displayKey; /** The value attribute of the control. */ @property({ reflect: false, type: String }) @@ -73,11 +92,8 @@ export default class IgcComboComponent extends LitElement { @property({ attribute: false }) public data: Array = []; - @queryAll('igc-combo-item') - public items!: NodeListOf; - @state() - public dataState: Array = []; + public dataState: Array> = []; @watch('data') protected dataChanged() { @@ -92,53 +108,63 @@ export default class IgcComboComponent extends LitElement { @watch('groupKey') protected groupItems() { if (!this.groupKey) return; - - this.dataState = Object.values( - this.dataState.reduce((acc: any, obj: any) => { - const key = obj[this.groupKey]; - - if (!acc[key]) { - acc[key] = []; - acc[key].push({ - [this.valueKey as string]: key, - [this.displayKey as string]: key, - [this.groupKey as string]: key, - header: true, - }); - } - acc[key].push(Object.assign(obj, { header: false })); - return acc; - }, {}) - ); - - this.dataState = this.dataState.flat(); + this.dataState = this.dataController.group(this.dataState); } @property({ attribute: false }) - public itemTemplate: (item: T) => TemplateResult = (item) => { + public itemTemplate: (item: T) => TemplateResult = (item) => { if (this.displayKey) { - return html`${(item as any)[this.displayKey]}`; + return html`${item[this.displayKey]}`; } return html`${item}`; }; @property({ attribute: false }) - public headerItemTemplate: (item: T) => TemplateResult = (item) => { - return html`${(item as any)[this.groupKey]}`; + public headerItemTemplate: (item: ComboRecord) => TemplateResult = ( + item + ) => { + return html`${item[this.groupKey!]}`; }; - protected itemRenderer = (item: T, index: number): TemplateResult => { + constructor() { + super(); + + this.toggleController = new IgcToggleController(this, { + target: this.target, + closeCallback: () => {}, + }); + } + + public show() { + if (this.open) return; + this.open = true; + } + + public hide() { + if (!this.open) return; + this.open = false; + } + + public toggle() { + this.open ? this.hide() : this.show(); + } + + protected itemRenderer = ( + item: ComboRecord, + index: number + ): TemplateResult => { const headerTemplate = html`${this.headerItemTemplate(item)}`; const itemTemplate = html`${this.itemTemplate(item)}`; - return html`${(item as any)?.header ? headerTemplate : itemTemplate}`; + return html`${item?.header ? headerTemplate : itemTemplate}`; }; public scrollToIndex(index: number, position?: string) { @@ -152,7 +178,17 @@ export default class IgcComboComponent extends LitElement { public override render() { return html` -
+
+
${virtualize({ scroller: true, diff --git a/src/components/combo/controllers/data.ts b/src/components/combo/controllers/data.ts new file mode 100644 index 000000000..9ffbc5a76 --- /dev/null +++ b/src/components/combo/controllers/data.ts @@ -0,0 +1,18 @@ +import { ReactiveController } from 'lit'; +import GroupDataOperation from '../operations/group.js'; +import { ComboHost } from '../types.js'; + +export class DataController implements ReactiveController { + protected grouping = new GroupDataOperation(); + // protected filtering = new FilterDataOperation(); + + constructor(protected host: ComboHost) { + this.host.addController(this); + } + + public hostConnected() {} + + public group(data: T[]) { + return this.grouping.apply(data, this.host); + } +} diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts index 9d5643fd8..db7a23775 100644 --- a/src/components/combo/controllers/navigation.ts +++ b/src/components/combo/controllers/navigation.ts @@ -1,5 +1,6 @@ import { ReactiveController, ReactiveControllerHost } from 'lit'; -import IgcComboComponent from '../combo'; +import IgcComboComponent from '../combo.js'; +import { ComboRecord } from '../types.js'; type ComboHost = ReactiveControllerHost & IgcComboComponent; @@ -31,7 +32,9 @@ export class NavigationController } protected get firstItem() { - return this.host.dataState.findIndex((i: any) => i.header !== true); + return this.host.dataState.findIndex( + (i: ComboRecord) => i.header !== true + ); } protected get lastItem() { @@ -56,7 +59,7 @@ export class NavigationController this.host.scrollToIndex(this.active); } - protected async end() { + protected end() { this.active = this.lastItem; this.host.scrollToIndex(this.active); } @@ -82,7 +85,7 @@ export class NavigationController let index = startIndex; const items = this.host.dataState; - if ((items[index + direction] as any)?.header) { + if (items[index + direction]?.header) { this.getNearestItem((index += direction), direction); } diff --git a/src/components/combo/operations/group.ts b/src/components/combo/operations/group.ts new file mode 100644 index 000000000..75dafba70 --- /dev/null +++ b/src/components/combo/operations/group.ts @@ -0,0 +1,30 @@ +// import type { Values } from '../internal/types.js'; +// import type { SortExpression, SortState } from './sort/types.js'; + +import { ComboHost, ComboRecord, Keys } from '../types.js'; + +export default class GroupDataOperation { + public apply(data: T[], host: ComboHost) { + const { groupKey, valueKey, displayKey } = host; + const result = new Map(); + + data.forEach((item: T) => { + const key = item[groupKey!]; + const group = result.get(key) ?? >[]; + + if (group.length === 0) { + group.push({ + [valueKey as Keys]: key, + [displayKey as Keys]: key, + [groupKey as Keys]: key, + header: true, + }); + } + + group.push(Object.assign(item, { header: false })); + result.set(key, group); + }); + + return [...result.values()].flat() as Array>; + } +} diff --git a/src/components/combo/types.ts b/src/components/combo/types.ts new file mode 100644 index 000000000..11425f199 --- /dev/null +++ b/src/components/combo/types.ts @@ -0,0 +1,14 @@ +import { ReactiveControllerHost } from 'lit'; +import IgcComboComponent from './combo.js'; + +export type Keys = keyof T; +export type Values = T[keyof T]; + +export interface ComboRecordMeta { + header: boolean; +} + +export type ComboRecord = T & ComboRecordMeta; + +export type ComboHost = ReactiveControllerHost & + IgcComboComponent; diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 48aa1a929..32b55e84a 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -21,12 +21,62 @@ const metadata = { description: 'The name attribute of the control.', control: 'text', }, + scrollStrategy: { + type: '"scroll" | "block" | "close" | undefined', + options: ['scroll', 'block', 'close', 'undefined'], + control: { + type: 'inline-radio', + }, + }, + keepOpenOnOutsideClick: { + type: 'boolean | undefined', + control: 'boolean | undefined', + }, + open: { + type: 'boolean', + description: 'Sets the open state of the component.', + control: 'boolean', + defaultValue: false, + }, + positionStrategy: { + type: '"absolute" | "fixed" | undefined', + description: + 'The positioning strategy to use.\nUse the `fixed` strategy when the target element is in a fixed container, otherwise - use `absolute`.', + options: ['absolute', 'fixed', 'undefined'], + control: { + type: 'inline-radio', + }, + }, + flip: { + type: 'boolean | undefined', + description: + "Whether the element should be flipped to the opposite side once it's about to overflow the visible area.\nOnce enough space is detected on its preferred side, it will flip back.", + control: 'boolean | undefined', + }, + distance: { + type: 'number | undefined', + description: + 'Whether to prevent the element from being cut off by moving it so it stays visible within its boundary area.', + control: 'number', + }, + sameWidth: { + type: 'boolean | undefined', + description: 'Whether to make the toggle the same width as the target.', + control: 'boolean | undefined', + }, }, }; export default metadata; interface ArgTypes { value: string | undefined; name: string; + scrollStrategy: 'scroll' | 'block' | 'close' | undefined; + keepOpenOnOutsideClick: boolean | undefined; + open: boolean; + positionStrategy: 'absolute' | 'fixed' | undefined; + flip: boolean | undefined; + distance: number | undefined; + sameWidth: boolean | undefined; } // endregion @@ -75,7 +125,7 @@ function generateCities(amount = 200) { // .itemTemplate=${itemTemplate} // .headerItemTemplate=${headerItemTemplate} - +const cities = generateCities(200); const Template: Story = ( { name }: ArgTypes, { globals: { direction } }: Context @@ -86,7 +136,7 @@ const Template: Story = ( value-key="id" display-key="name" group-key="country" - .data=${generateCities(200)} + .data=${cities} > `; diff --git a/stories/linear-progress.stories.ts b/stories/linear-progress.stories.ts index 37147ca89..4d4003b54 100644 --- a/stories/linear-progress.stories.ts +++ b/stories/linear-progress.stories.ts @@ -14,13 +14,13 @@ const metadata = { defaultValue: false, }, labelAlign: { - type: '"top" | "bottom" | "top-start" | "top-end" | "bottom-start" | "bottom-end"', + type: '"top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end"', description: 'The position for the default label of the control.', options: [ 'top', - 'bottom', 'top-start', 'top-end', + 'bottom', 'bottom-start', 'bottom-end', ], @@ -81,9 +81,9 @@ interface ArgTypes { striped: boolean; labelAlign: | 'top' - | 'bottom' | 'top-start' | 'top-end' + | 'bottom' | 'bottom-start' | 'bottom-end'; max: number; diff --git a/stories/nav-drawer.stories.ts b/stories/nav-drawer.stories.ts index 1fcd44903..47928546b 100644 --- a/stories/nav-drawer.stories.ts +++ b/stories/nav-drawer.stories.ts @@ -13,9 +13,9 @@ const metadata = { component: 'igc-nav-drawer', argTypes: { position: { - type: '"start" | "end" | "top" | "bottom" | "relative"', + type: '"top" | "bottom" | "start" | "end" | "relative"', description: 'The position of the drawer.', - options: ['start', 'end', 'top', 'bottom', 'relative'], + options: ['top', 'bottom', 'start', 'end', 'relative'], control: { type: 'select', }, @@ -31,7 +31,7 @@ const metadata = { }; export default metadata; interface ArgTypes { - position: 'start' | 'end' | 'top' | 'bottom' | 'relative'; + position: 'top' | 'bottom' | 'start' | 'end' | 'relative'; open: boolean; } // endregion From b31d406af7cd9542beab167a4b82a75cbbcaf183 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Sun, 27 Nov 2022 15:31:11 +0200 Subject: [PATCH 08/82] feat(combo): add item selection and value --- src/components/combo/combo.ts | 76 ++++++++++++++++++- .../combo/controllers/navigation.ts | 17 ++++- src/components/combo/operations/group.ts | 7 +- stories/combo.stories.ts | 2 +- 4 files changed, 88 insertions(+), 14 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 6214c3920..9b7c2f38d 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -11,13 +11,19 @@ import { watch } from '../common/decorators/watch.js'; import { defineComponents } from '../common/definitions/defineComponents.js'; import IgcComboItemComponent from './combo-item.js'; import IgcComboHeaderComponent from './combo-header.js'; +import IgcInputComponent from '../input/input.js'; import { NavigationController } from './controllers/navigation.js'; import { IgcToggleController } from '../toggle/toggle.controller.js'; import { IgcToggleComponent } from '../toggle/types.js'; import { Keys, ComboRecord } from './types.js'; import { DataController } from './controllers/data.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; -defineComponents(IgcComboItemComponent, IgcComboHeaderComponent); +defineComponents( + IgcComboItemComponent, + IgcComboHeaderComponent, + IgcInputComponent +); /** * @element igc-combo @@ -95,6 +101,9 @@ export default class IgcComboComponent @state() public dataState: Array> = []; + @state() + protected selected: Set = new Set(); + @watch('data') protected dataChanged() { this.dataState = structuredClone(this.data); @@ -111,6 +120,21 @@ export default class IgcComboComponent this.dataState = this.dataController.group(this.dataState); } + @watch('selected', { waitUntilFirstUpdate: true }) + protected updateValue() { + const values = Array.from(this.selected.values()); + + this.value = values + .map((value) => { + if (typeof value === 'object') { + return value[this.valueKey!]; + } else { + return value; + } + }) + .join(', '); + } + @property({ attribute: false }) public itemTemplate: (item: T) => TemplateResult = (item) => { if (this.displayKey) { @@ -136,6 +160,34 @@ export default class IgcComboComponent }); } + public select(items?: T[]) { + if (!items || items.length === 0) { + this.dataState + .filter((i) => !i.header) + .forEach((item) => { + this.selected.add(item); + }); + } + + items?.forEach((item) => { + this.selected.add(item); + }); + + this.requestUpdate('selected'); + } + + public deselect(items?: T[]) { + if (!items || items.length === 0) { + this.selected.clear(); + } + + items?.forEach((item) => { + this.selected.delete(item); + }); + + this.requestUpdate('selected'); + } + public show() { if (this.open) return; this.open = true; @@ -159,8 +211,10 @@ export default class IgcComboComponent >`; const itemTemplate = html`${this.itemTemplate(item)}`; @@ -176,13 +230,27 @@ export default class IgcComboComponent this.navigationController.navigate(event); } + protected itemClickHandler(event: MouseEvent) { + const target = event.target as IgcComboItemComponent; + this.toggleItem(target.index); + } + + public toggleItem(index: number) { + const target = Array.from(this.items).find((i) => i.index === index); + const item = this.dataState[index]; + target!.selected = !target!.selected; + target!.selected ? this.select([item]) : this.deselect([item]); + this.navigationController.active = target!.index; + } + public override render() { return html` -
+ .value=${ifDefined(this.value)} + readonly + >
Object.entries({ ArrowDown: this.arrowDown, ArrowUp: this.arrowUp, + ' ': this.space, + Enter: this.enter, Home: this.home, End: this.end, }) @@ -47,6 +49,7 @@ export class NavigationController public set active(node: number) { this._active = node; + this.host.scrollToIndex(node); this.host.requestUpdate(); } @@ -56,12 +59,20 @@ export class NavigationController protected home() { this.active = this.firstItem; - this.host.scrollToIndex(this.active); } protected end() { this.active = this.lastItem; - this.host.scrollToIndex(this.active); + } + + protected space() { + if (this.active !== -1) { + this.host.toggleItem(this.active); + } + } + + protected enter() { + this.space(); } protected arrowDown() { @@ -76,9 +87,7 @@ export class NavigationController const next = this.getNearestItem(this.currentItem, direction); if (next === -1) return; - this.active = next; - this.host.scrollToIndex(this.active); } protected getNearestItem(startIndex: number, direction: number) { diff --git a/src/components/combo/operations/group.ts b/src/components/combo/operations/group.ts index 75dafba70..d89e36168 100644 --- a/src/components/combo/operations/group.ts +++ b/src/components/combo/operations/group.ts @@ -1,6 +1,3 @@ -// import type { Values } from '../internal/types.js'; -// import type { SortExpression, SortState } from './sort/types.js'; - import { ComboHost, ComboRecord, Keys } from '../types.js'; export default class GroupDataOperation { @@ -21,10 +18,10 @@ export default class GroupDataOperation { }); } - group.push(Object.assign(item, { header: false })); + group.push(item); result.set(key, group); }); - return [...result.values()].flat() as Array>; + return Array.from(result.values()).flat(); } } diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 32b55e84a..f40662b7a 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -133,7 +133,7 @@ const Template: Story = ( Date: Sun, 27 Nov 2022 19:30:19 +0200 Subject: [PATCH 09/82] refactor(combo): item selection and activation --- src/components/combo/combo-item.ts | 6 ------ src/components/combo/combo.ts | 8 +++----- src/components/combo/controllers/navigation.ts | 1 + .../combo/themes/light/item/combo-item.base.scss | 4 ++++ 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/components/combo/combo-item.ts b/src/components/combo/combo-item.ts index daf4aa968..9aa814a55 100644 --- a/src/components/combo/combo-item.ts +++ b/src/components/combo/combo-item.ts @@ -16,9 +16,6 @@ export default class IgcComboItemComponent extends LitElement { public static readonly tagName: string = 'igc-combo-item'; public static override styles = styles; - @property({ attribute: false }) - public activeNode!: number; - @property({ attribute: false }) public index!: number; @@ -39,7 +36,6 @@ export default class IgcComboItemComponent extends LitElement { this.selected ? this.setAttribute('aria-selected', 'true') : this.removeAttribute('aria-selected'); - this.active = this.selected; } public override connectedCallback() { @@ -48,8 +44,6 @@ export default class IgcComboItemComponent extends LitElement { } protected override render() { - this.active = this.activeNode === this.index; - return html`
diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 9b7c2f38d..7bbdfe4e6 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -213,7 +213,7 @@ export default class IgcComboComponent const itemTemplate = html`${this.itemTemplate(item)}`; @@ -236,11 +236,9 @@ export default class IgcComboComponent } public toggleItem(index: number) { - const target = Array.from(this.items).find((i) => i.index === index); const item = this.dataState[index]; - target!.selected = !target!.selected; - target!.selected ? this.select([item]) : this.deselect([item]); - this.navigationController.active = target!.index; + !this.selected.has(item) ? this.select([item]) : this.deselect([item]); + this.navigationController.active = index; } public override render() { diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts index e9c966101..9cd860e6b 100644 --- a/src/components/combo/controllers/navigation.ts +++ b/src/components/combo/controllers/navigation.ts @@ -73,6 +73,7 @@ export class NavigationController protected enter() { this.space(); + this.host.open = false; } protected arrowDown() { diff --git a/src/components/combo/themes/light/item/combo-item.base.scss b/src/components/combo/themes/light/item/combo-item.base.scss index 9238677e4..d7a2710e0 100644 --- a/src/components/combo/themes/light/item/combo-item.base.scss +++ b/src/components/combo/themes/light/item/combo-item.base.scss @@ -4,3 +4,7 @@ :host { gap: rem(8px); } + +igc-checkbox { + pointer-events: none; +} From 836c66d6173e9df5cfd7a3992fbb76f6c836f4bb Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Sun, 27 Nov 2022 21:16:07 +0200 Subject: [PATCH 10/82] themes(input): bootstrap slotted suffix and prefix --- .../input/themes/light/input.bootstrap.scss | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/input/themes/light/input.bootstrap.scss b/src/components/input/themes/light/input.bootstrap.scss index 1d6a534bc..5c8357b31 100644 --- a/src/components/input/themes/light/input.bootstrap.scss +++ b/src/components/input/themes/light/input.bootstrap.scss @@ -84,8 +84,6 @@ $input-background: var(--input-background, #fff) !default; background: color(gray, 300); transition: box-shadow .15s ease-out, border .15s ease-out; font-size: var(--font); - padding-inline: pad-inline(8px, 12px, 16px); - padding-block: pad-block(4px, 6px, 8px); &:focus-within { box-shadow: 0 0 0 rem(4px) color(gray, 300, .38); @@ -114,6 +112,16 @@ $input-background: var(--input-background, #fff) !default; } } + [name='prefix']::slotted(*), + [name='suffix']::slotted(*) { + display: inline-flex; + align-items: center; + width: fit-content; + height: 100%; + padding-inline: pad-inline(8px, 12px, 16px); + padding-block: pad-block(4px, 6px, 8px); + } + [part='suffix'] { grid-area: 1 / 3; From 959b51be3dd9954c1a27e01738cf77cc323cecff Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Sun, 27 Nov 2022 21:20:07 +0200 Subject: [PATCH 11/82] themes(combo): bootstrap clear button --- src/components/combo/themes/light/combo.bootstrap.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/combo/themes/light/combo.bootstrap.scss b/src/components/combo/themes/light/combo.bootstrap.scss index e69de29bb..f60b13971 100644 --- a/src/components/combo/themes/light/combo.bootstrap.scss +++ b/src/components/combo/themes/light/combo.bootstrap.scss @@ -0,0 +1,5 @@ +@use '../../../../styles/utilities' as *; + +[part='clear-icon'] { + border-inline-end: 1px solid color(gray, 400); +} From 7400758ad60b0502c81642ac5c473a8bbc0a3a50 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Sun, 27 Nov 2022 21:20:47 +0200 Subject: [PATCH 12/82] refactor(combo-item): checkbox not focusable --- src/components/combo/combo-item.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/combo/combo-item.ts b/src/components/combo/combo-item.ts index 9aa814a55..32ea1e4ac 100644 --- a/src/components/combo/combo-item.ts +++ b/src/components/combo/combo-item.ts @@ -46,7 +46,7 @@ export default class IgcComboItemComponent extends LitElement { protected override render() { return html`
- +
From 518a1a170b8c277e28a23fbe6b02b10c76b98043 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Sun, 27 Nov 2022 21:21:10 +0200 Subject: [PATCH 13/82] feat(combo): add clear icon --- src/components/combo/combo.ts | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 7bbdfe4e6..ae8bfdd47 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -241,6 +241,12 @@ export default class IgcComboComponent this.navigationController.active = index; } + protected handleClearIconClick(e: MouseEvent) { + e.stopPropagation(); + this.deselect(); + this.navigationController.active = 0; + } + public override render() { return html` @click=${this.toggle} .value=${ifDefined(this.value)} readonly - > + > + + + + + + + + + + +
Date: Sun, 27 Nov 2022 22:20:12 +0200 Subject: [PATCH 14/82] refactor(combo): allow selection without valueKey --- src/components/combo/combo.ts | 2 +- stories/combo.stories.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index ae8bfdd47..7094eacff 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -127,7 +127,7 @@ export default class IgcComboComponent this.value = values .map((value) => { if (typeof value === 'object') { - return value[this.valueKey!]; + return this.displayKey ? value[this.displayKey] : value; } else { return value; } diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index f40662b7a..32b55e84a 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -133,7 +133,7 @@ const Template: Story = ( Date: Sun, 27 Nov 2022 23:42:24 +0200 Subject: [PATCH 15/82] feat(combo): select by valueKey and object values --- src/components/combo/combo.ts | 96 +++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 16 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 7094eacff..d8c9a1fef 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -15,7 +15,7 @@ import IgcInputComponent from '../input/input.js'; import { NavigationController } from './controllers/navigation.js'; import { IgcToggleController } from '../toggle/toggle.controller.js'; import { IgcToggleComponent } from '../toggle/types.js'; -import { Keys, ComboRecord } from './types.js'; +import { Keys, ComboRecord, Values } from './types.js'; import { DataController } from './controllers/data.js'; import { ifDefined } from 'lit/directives/if-defined.js'; @@ -160,30 +160,86 @@ export default class IgcComboComponent }); } - public select(items?: T[]) { + private selectValueKeys(values: Values[]) { + if (values.length === 0) return; + + values.forEach((value) => { + const item = this.dataState.find((i) => i[this.valueKey!] === value); + + if (item) { + this.selected.add(item); + } + }); + } + + private deselectValueKeys(values: Values[]) { + if (values.length === 0) return; + + values.forEach((value) => { + const item = this.dataState.find((i) => i[this.valueKey!] === value); + + if (item) { + this.selected.delete(item); + } + }); + } + + private selectObjects(items: T[]) { + if (items.length === 0) return; + + items.forEach((item) => { + this.selected.add(item as ComboRecord); + }); + } + + private deselectObjects(items: T[]) { + if (items.length === 0) return; + + items.forEach((item) => { + this.selected.delete(item as ComboRecord); + }); + } + + private selectAll() { + this.dataState + .filter((i) => !i.header) + .forEach((item) => { + this.selected.add(item); + }); + this.requestUpdate('selected'); + } + + private deselectAll() { + this.selected.clear(); + this.requestUpdate('selected'); + } + + public select(items?: T[] | Values[]) { if (!items || items.length === 0) { - this.dataState - .filter((i) => !i.header) - .forEach((item) => { - this.selected.add(item); - }); + this.selectAll(); + return; } - items?.forEach((item) => { - this.selected.add(item); - }); + if (this.valueKey) { + this.selectValueKeys(items as Values[]); + } else { + this.selectObjects(items as T[]); + } this.requestUpdate('selected'); } - public deselect(items?: T[]) { + public deselect(items?: T[] | Values[]) { if (!items || items.length === 0) { - this.selected.clear(); + this.deselectAll(); + return; } - items?.forEach((item) => { - this.selected.delete(item); - }); + if (this.valueKey) { + this.deselectValueKeys(items as Values[]); + } else { + this.deselectObjects(items as T[]); + } this.requestUpdate('selected'); } @@ -237,7 +293,15 @@ export default class IgcComboComponent public toggleItem(index: number) { const item = this.dataState[index]; - !this.selected.has(item) ? this.select([item]) : this.deselect([item]); + + if (this.valueKey) { + !this.selected.has(item) + ? this.select([item[this.valueKey]]) + : this.deselect([item[this.valueKey]]); + } else { + !this.selected.has(item) ? this.select([item]) : this.deselect([item]); + } + this.navigationController.active = index; } From 74eb53aab0c8b85ca5a6a47a8def44da82be8ff6 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 28 Nov 2022 09:59:40 +0200 Subject: [PATCH 16/82] refactor(combo): add slots and filter input --- src/components/combo/combo.ts | 5 +++++ src/components/combo/themes/light/combo.base.scss | 8 ++++++-- stories/combo.stories.ts | 5 ++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index d8c9a1fef..eca0116b2 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -350,6 +350,10 @@ export default class IgcComboComponent part="list-wrapper" ${this.toggleController.toggleDirective} > +
+ +
+
${virtualize({ scroller: true, @@ -361,6 +365,7 @@ export default class IgcComboComponent }, })}
+
`; } diff --git a/src/components/combo/themes/light/combo.base.scss b/src/components/combo/themes/light/combo.base.scss index b397492f6..2cc1ab609 100644 --- a/src/components/combo/themes/light/combo.base.scss +++ b/src/components/combo/themes/light/combo.base.scss @@ -1,6 +1,10 @@ @use '../../../../styles/common/component'; @use '../../../../styles/utilities' as *; -:host { - color: red; +[part='list-wrapper'] { + height: fit-content; +} + +[part='list'] { + min-height: 300px !important; } diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 32b55e84a..74e7a1f42 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -137,7 +137,10 @@ const Template: Story = ( display-key="name" group-key="country" .data=${cities} - > + > +
This is a custom header
+
This is a custom footer
+ `; export const Basic = Template.bind({}); From 7e8b4ee6c91e1d2b47ee5aaa9f7e9bf4d2cb7c8a Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 28 Nov 2022 11:48:23 +0200 Subject: [PATCH 17/82] refactor(combo): improve mouse selection --- src/components/combo/combo-list.ts | 17 ++++++++ src/components/combo/combo.ts | 40 +++++++++---------- .../combo/controllers/navigation.ts | 18 +++++---- 3 files changed, 46 insertions(+), 29 deletions(-) create mode 100644 src/components/combo/combo-list.ts diff --git a/src/components/combo/combo-list.ts b/src/components/combo/combo-list.ts new file mode 100644 index 000000000..a6a9ec27a --- /dev/null +++ b/src/components/combo/combo-list.ts @@ -0,0 +1,17 @@ +import { LitVirtualizer } from '@lit-labs/virtualizer'; + +export default class IgcComboListComponent extends LitVirtualizer { + public static readonly tagName = 'igc-combo-list'; + public override scroller = true; + + public override connectedCallback() { + super.connectedCallback(); + this.setAttribute('tabindex', '0'); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'igc-combo-list': IgcComboListComponent; + } +} diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index eca0116b2..a5251e89f 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -6,9 +6,9 @@ import { styles as material } from './themes/light/combo.material.css.js'; import { styles as fluent } from './themes/light/combo.fluent.css.js'; import { styles as indigo } from './themes/light/combo.indigo.css.js'; import { property, query, queryAll, state } from 'lit/decorators.js'; -import { virtualize } from '@lit-labs/virtualizer/virtualize.js'; import { watch } from '../common/decorators/watch.js'; import { defineComponents } from '../common/definitions/defineComponents.js'; +import IgcComboListComponent from './combo-list.js'; import IgcComboItemComponent from './combo-item.js'; import IgcComboHeaderComponent from './combo-header.js'; import IgcInputComponent from '../input/input.js'; @@ -20,6 +20,7 @@ import { DataController } from './controllers/data.js'; import { ifDefined } from 'lit/directives/if-defined.js'; defineComponents( + IgcComboListComponent, IgcComboItemComponent, IgcComboHeaderComponent, IgcInputComponent @@ -64,9 +65,6 @@ export default class IgcComboComponent protected dataController = new DataController(this); protected toggleController!: IgcToggleController; - private scrollIndex = 0; - private scrollPosition = 'center'; - @query('[part="target"]') private target!: HTMLElement; @@ -267,7 +265,7 @@ export default class IgcComboComponent >`; const itemTemplate = html` return html`${item?.header ? headerTemplate : itemTemplate}`; }; - public scrollToIndex(index: number, position?: string) { - this.scrollIndex = index; - if (position) this.scrollPosition = position; - } - protected keydownHandler(event: KeyboardEvent) { - this.navigationController.navigate(event); + const target = event + .composedPath() + .find( + (el) => el instanceof IgcComboListComponent + ) as IgcComboListComponent; + + if (target) { + this.navigationController.navigate(event, target); + } } protected itemClickHandler(event: MouseEvent) { @@ -354,17 +355,12 @@ export default class IgcComboComponent
-
- ${virtualize({ - scroller: true, - items: this.dataState, - renderItem: this.itemRenderer, - scrollToIndex: { - index: this.scrollIndex, - position: this.scrollPosition, - }, - })} -
+ +
`; diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts index 9cd860e6b..bcb4a6891 100644 --- a/src/components/combo/controllers/navigation.ts +++ b/src/components/combo/controllers/navigation.ts @@ -1,4 +1,5 @@ import { ReactiveController, ReactiveControllerHost } from 'lit'; +import IgcComboListComponent from '../combo-list.js'; import IgcComboComponent from '../combo.js'; import { ComboRecord } from '../types.js'; @@ -49,7 +50,6 @@ export class NavigationController public set active(node: number) { this._active = node; - this.host.scrollToIndex(node); this.host.requestUpdate(); } @@ -57,12 +57,14 @@ export class NavigationController this.host.addController(this); } - protected home() { + protected home(container: IgcComboListComponent) { this.active = this.firstItem; + container.scrollToIndex(this.active, 'center'); } - protected end() { + protected end(container: IgcComboListComponent) { this.active = this.lastItem; + container.scrollToIndex(this.active, 'center'); } protected space() { @@ -76,12 +78,14 @@ export class NavigationController this.host.open = false; } - protected arrowDown() { + protected arrowDown(container: IgcComboListComponent) { this.getNextItem(DIRECTION.Down); + container.scrollToIndex(this.active, 'center'); } - protected arrowUp() { + protected arrowUp(container: IgcComboListComponent) { this.getNextItem(DIRECTION.Up); + container.scrollToIndex(this.active, 'center'); } protected getNextItem(direction: DIRECTION) { @@ -114,10 +118,10 @@ export class NavigationController this.active = START_INDEX; } - public navigate(event: KeyboardEvent) { + public navigate(event: KeyboardEvent, container: IgcComboListComponent) { if (this.handlers.has(event.key)) { event.preventDefault(); - this.handlers.get(event.key)!.call(this); + this.handlers.get(event.key)!.call(this, container); } } } From 28c33a1f508b3f6fb198cbe5dfc75723a0ca328a Mon Sep 17 00:00:00 2001 From: Marin Popov Date: Mon, 28 Nov 2022 18:35:58 +0200 Subject: [PATCH 18/82] Add Combo theming (#561) * style(combo): Add light and dark theme styles * style(combo): fix missing separator for clear icon * style(combo): fix font-family for header and footer -make the sample more presentable. --- src/components/combo/combo-item.ts | 2 +- src/components/combo/combo.ts | 8 +-- .../combo/themes/dark/combo.bootstrap.scss | 5 ++ .../combo/themes/dark/combo.fluent.scss | 5 ++ .../combo/themes/dark/combo.indigo.scss | 7 +++ .../combo/themes/dark/combo.material.scss | 7 +++ .../combo/themes/light/combo.base.scss | 53 ++++++++++++++++++- .../combo/themes/light/combo.bootstrap.scss | 1 + .../combo/themes/light/combo.fluent.scss | 1 + .../combo/themes/light/combo.indigo.scss | 1 + .../combo/themes/light/combo.material.scss | 1 + src/styles/themes/dark/bootstrap.scss | 2 + src/styles/themes/dark/fluent.scss | 2 + src/styles/themes/dark/indigo.scss | 2 + src/styles/themes/dark/material.scss | 2 + stories/combo.stories.ts | 9 +++- 16 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 src/components/combo/themes/dark/combo.bootstrap.scss create mode 100644 src/components/combo/themes/dark/combo.fluent.scss create mode 100644 src/components/combo/themes/dark/combo.indigo.scss create mode 100644 src/components/combo/themes/dark/combo.material.scss diff --git a/src/components/combo/combo-item.ts b/src/components/combo/combo-item.ts index 32ea1e4ac..f41da20a7 100644 --- a/src/components/combo/combo-item.ts +++ b/src/components/combo/combo-item.ts @@ -1,6 +1,6 @@ import { html, LitElement } from 'lit'; import { themes } from '../../theming/theming-decorator.js'; -import { styles } from './themes/light/item/combo-item.base.css'; +import { styles } from './themes/light/item/combo-item.base.css.js'; import { styles as bootstrap } from '../dropdown/themes/light/dropdown-item.bootstrap.css'; import { styles as fluent } from '../dropdown/themes/light/dropdown-item.fluent.css'; import { styles as indigo } from '../dropdown/themes/light/dropdown-item.indigo.css'; diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index a5251e89f..a6e948418 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -316,6 +316,7 @@ export default class IgcComboComponent return html` @@ -335,7 +335,7 @@ export default class IgcComboComponent > - + ${this.toggleController.toggleDirective} >
- +
* { + width: 100%; + } } diff --git a/src/components/combo/themes/light/combo.bootstrap.scss b/src/components/combo/themes/light/combo.bootstrap.scss index f60b13971..816354d4b 100644 --- a/src/components/combo/themes/light/combo.bootstrap.scss +++ b/src/components/combo/themes/light/combo.bootstrap.scss @@ -1,4 +1,5 @@ @use '../../../../styles/utilities' as *; +@use '../../../select/themes/light/select.bootstrap' as *; [part='clear-icon'] { border-inline-end: 1px solid color(gray, 400); diff --git a/src/components/combo/themes/light/combo.fluent.scss b/src/components/combo/themes/light/combo.fluent.scss index e69de29bb..859d50372 100644 --- a/src/components/combo/themes/light/combo.fluent.scss +++ b/src/components/combo/themes/light/combo.fluent.scss @@ -0,0 +1 @@ +@use '../../../select/themes/light/select.fluent' as *; diff --git a/src/components/combo/themes/light/combo.indigo.scss b/src/components/combo/themes/light/combo.indigo.scss index e69de29bb..671065b89 100644 --- a/src/components/combo/themes/light/combo.indigo.scss +++ b/src/components/combo/themes/light/combo.indigo.scss @@ -0,0 +1 @@ +@use '../../../select/themes/light/select.indigo' as *; diff --git a/src/components/combo/themes/light/combo.material.scss b/src/components/combo/themes/light/combo.material.scss index e69de29bb..fd4432949 100644 --- a/src/components/combo/themes/light/combo.material.scss +++ b/src/components/combo/themes/light/combo.material.scss @@ -0,0 +1 @@ +@use '../../../select/themes/light/select.material' as *; diff --git a/src/styles/themes/dark/bootstrap.scss b/src/styles/themes/dark/bootstrap.scss index a4d1a0378..cd642e59c 100644 --- a/src/styles/themes/dark/bootstrap.scss +++ b/src/styles/themes/dark/bootstrap.scss @@ -10,6 +10,7 @@ @use '../../../components/tree/themes/dark/tree-item.bootstrap' as tree-item; @use '../../../components/select/themes/dark/select.bootstrap' as select; @use '../../../components/nav-drawer/themes/dark/nav-drawer-item.bootstrap' as nav-drawer-item; +@use '../../../components/combo/themes/dark/combo.bootstrap' as combo; @include base.root-styles($dark-bootstrap-palette); @include dropdown.theme(); @include input.theme(); @@ -20,3 +21,4 @@ @include nav-drawer-item.theme(); @include select.theme(); @include grid.theme(); +@include combo.theme(); diff --git a/src/styles/themes/dark/fluent.scss b/src/styles/themes/dark/fluent.scss index 6a7358b7d..d2fa185e5 100644 --- a/src/styles/themes/dark/fluent.scss +++ b/src/styles/themes/dark/fluent.scss @@ -15,6 +15,7 @@ @use '../../../components/tabs/themes/dark/tab.dark.base' as tab; @use '../../../components/tree/themes/dark/tree-item.fluent' as tree-item; @use '../../../components/nav-drawer/themes/dark/nav-drawer-item.fluent' as nav-drawer-item; +@use '../../../components/combo/themes/dark/combo.fluent' as combo; @include base.root-styles($dark-fluent-palette); @include button.theme(); @include icon-button.theme(); @@ -30,3 +31,4 @@ @include tree-item.theme(); @include nav-drawer-item.theme(); @include grid.theme(); +@include combo.theme(); diff --git a/src/styles/themes/dark/indigo.scss b/src/styles/themes/dark/indigo.scss index 06f1c1810..38f3218cf 100644 --- a/src/styles/themes/dark/indigo.scss +++ b/src/styles/themes/dark/indigo.scss @@ -14,6 +14,7 @@ @use '../../../components/nav-drawer/themes/dark/nav-drawer-item.indigo' as nav-drawer-item; @use '../../../components/select/themes/dark/select.indigo' as select; @use '../../../components/snackbar/themes/dark/snackbar.indigo' as snackbar; +@use '../../../components/combo/themes/dark/combo.indigo' as combo; @include base.root-styles($dark-indigo-palette); @include checkbox.theme(); @include switch.theme(); @@ -28,3 +29,4 @@ @include select.theme(); @include snackbar.theme(); @include grid.theme(); +@include combo.theme(); diff --git a/src/styles/themes/dark/material.scss b/src/styles/themes/dark/material.scss index ecaedb80e..e187f70a6 100644 --- a/src/styles/themes/dark/material.scss +++ b/src/styles/themes/dark/material.scss @@ -6,9 +6,11 @@ @use '../../../components/select/themes/dark/select.material' as select; @use '../../../components/tabs/themes/dark/tab.dark.base' as tab; @use '../../../components/nav-drawer/themes/dark/nav-drawer-item.base.scss' as nav-drawer-item; +@use '../../../components/combo/themes/dark/combo.material' as combo; @include base.root-styles($dark-material-palette); @include input.theme(); @include select.theme(); @include tab.theme(); @include nav-drawer-item.theme(); @include grid.theme(); +@include combo.theme(); diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 74e7a1f42..e82d23b5e 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -131,6 +131,7 @@ const Template: Story = ( { globals: { direction } }: Context ) => html` = ( group-key="country" .data=${cities} > -
This is a custom header
-
This is a custom footer
+
+ This is a custom header +
+
+ This is a custom footer +
`; From 8870f635ebe1acc478e076546ecec1a23745af63 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 29 Nov 2022 13:44:09 +0200 Subject: [PATCH 19/82] feat(combo): filtering and grouping --- src/components/combo/combo-item.ts | 6 +- src/components/combo/combo.ts | 90 ++++++++++++++----- src/components/combo/controllers/data.ts | 12 ++- .../combo/controllers/navigation.ts | 2 +- src/components/combo/operations/filter.ts | 22 +++++ src/components/combo/operations/group.ts | 53 ++++++++++- src/components/combo/types.ts | 7 ++ 7 files changed, 159 insertions(+), 33 deletions(-) create mode 100644 src/components/combo/operations/filter.ts diff --git a/src/components/combo/combo-item.ts b/src/components/combo/combo-item.ts index f41da20a7..e6d678ed4 100644 --- a/src/components/combo/combo-item.ts +++ b/src/components/combo/combo-item.ts @@ -1,9 +1,9 @@ import { html, LitElement } from 'lit'; import { themes } from '../../theming/theming-decorator.js'; import { styles } from './themes/light/item/combo-item.base.css.js'; -import { styles as bootstrap } from '../dropdown/themes/light/dropdown-item.bootstrap.css'; -import { styles as fluent } from '../dropdown/themes/light/dropdown-item.fluent.css'; -import { styles as indigo } from '../dropdown/themes/light/dropdown-item.indigo.css'; +import { styles as bootstrap } from '../dropdown/themes/light/dropdown-item.bootstrap.css.js'; +import { styles as fluent } from '../dropdown/themes/light/dropdown-item.fluent.css.js'; +import { styles as indigo } from '../dropdown/themes/light/dropdown-item.indigo.css.js'; import { property } from 'lit/decorators.js'; import { watch } from '../common/decorators/watch.js'; import IgcCheckboxComopnent from '../checkbox/checkbox.js'; diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index a6e948418..6346b4d7a 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -5,7 +5,7 @@ import { styles as bootstrap } from './themes/light/combo.bootstrap.css.js'; import { styles as material } from './themes/light/combo.material.css.js'; import { styles as fluent } from './themes/light/combo.fluent.css.js'; import { styles as indigo } from './themes/light/combo.indigo.css.js'; -import { property, query, queryAll, state } from 'lit/decorators.js'; +import { property, query, state } from 'lit/decorators.js'; import { watch } from '../common/decorators/watch.js'; import { defineComponents } from '../common/definitions/defineComponents.js'; import IgcComboListComponent from './combo-list.js'; @@ -15,9 +15,16 @@ import IgcInputComponent from '../input/input.js'; import { NavigationController } from './controllers/navigation.js'; import { IgcToggleController } from '../toggle/toggle.controller.js'; import { IgcToggleComponent } from '../toggle/types.js'; -import { Keys, ComboRecord, Values } from './types.js'; +import { + Keys, + ComboRecord, + Values, + GroupingDirection, + FilteringOptions, +} from './types.js'; import { DataController } from './controllers/data.js'; import { ifDefined } from 'lit/directives/if-defined.js'; +import { partNameMap } from '../common/util.js'; defineComponents( IgcComboListComponent, @@ -68,33 +75,48 @@ export default class IgcComboComponent @query('[part="target"]') private target!: HTMLElement; - @queryAll('igc-combo-item') - public items!: NodeListOf; + /** The data source used to build the list of options. */ + @property({ attribute: false }) + public data: Array = []; + + /** The value attribute of the control. */ + @property({ type: String, reflect: false }) + public value?: string | undefined; + + /** The name attribute of the control. */ + @property() + public name!: string; /** Sets the open state of the component. */ @property({ type: Boolean }) public open = false; - @property({ attribute: 'value-key' }) + @property({ attribute: 'value-key', reflect: false }) public valueKey?: Keys; - @property({ attribute: 'display-key' }) + @property({ attribute: 'display-key', reflect: false }) public displayKey?: Keys = this.valueKey; - @property({ attribute: 'group-key' }) + @property({ attribute: 'group-key', reflect: false }) public groupKey?: Keys = this.displayKey; - /** The value attribute of the control. */ - @property({ reflect: false, type: String }) - public value?: string | undefined; + @property({ attribute: 'group-sorting', reflect: false }) + public groupSorting?: GroupingDirection = 'asc'; - /** The name attribute of the control. */ - @property() - public name!: string; + @property({ attribute: 'filtering-options', reflect: false }) + public filteringOptions: FilteringOptions = { + filterKey: this.displayKey ?? null, + caseSensitive: false, + }; - /** The data source used to build the list of options. */ - @property({ attribute: false }) - public data: Array = []; + @property({ type: Boolean, attribute: 'case-sensitive-icon', reflect: false }) + public caseSensitiveIcon = false; + + @property({ type: Boolean, attribute: 'disable-filtering', reflect: false }) + public disableFiltering = false; + + @state() + public searchTerm = ''; @state() public dataState: Array> = []; @@ -113,9 +135,10 @@ export default class IgcComboComponent } @watch('groupKey') - protected groupItems() { - if (!this.groupKey) return; - this.dataState = this.dataController.group(this.dataState); + @watch('searchTerm') + protected pipeline() { + this.dataState = this.dataController.apply([...this.data]); + this.navigationController.active = 0; } @watch('selected', { waitUntilFirstUpdate: true }) @@ -242,6 +265,10 @@ export default class IgcComboComponent this.requestUpdate('selected'); } + protected handleSearchInput(e: CustomEvent) { + this.searchTerm = e.detail; + } + public show() { if (this.open) return; this.open = true; @@ -289,10 +316,10 @@ export default class IgcComboComponent protected itemClickHandler(event: MouseEvent) { const target = event.target as IgcComboItemComponent; - this.toggleItem(target.index); + this.toggleSelect(target.index); } - public toggleItem(index: number) { + public toggleSelect(index: number) { const item = this.dataState[index]; if (this.valueKey) { @@ -312,6 +339,10 @@ export default class IgcComboComponent this.navigationController.active = 0; } + protected toggleCaseSensitivity() { + this.filteringOptions.caseSensitive = !this.filteringOptions.caseSensitive; + } + public override render() { return html` part="list-wrapper" ${this.toggleController.toggleDirective} > -
+
+ @igcInput=${this.handleSearchInput} + @keydown=${(e: KeyboardEvent) => e.stopPropagation()} + > + +
implements ReactiveController { protected grouping = new GroupDataOperation(); - // protected filtering = new FilterDataOperation(); + protected filtering = new FilterDataOperation(); constructor(protected host: ComboHost) { this.host.addController(this); @@ -12,7 +13,10 @@ export class DataController implements ReactiveController { public hostConnected() {} - public group(data: T[]) { - return this.grouping.apply(data, this.host); + public apply(data: T[]) { + data = this.filtering.apply(data, this.host); + data = this.grouping.apply(data, this.host); + + return data as ComboRecord[]; } } diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts index bcb4a6891..82b2b3040 100644 --- a/src/components/combo/controllers/navigation.ts +++ b/src/components/combo/controllers/navigation.ts @@ -69,7 +69,7 @@ export class NavigationController protected space() { if (this.active !== -1) { - this.host.toggleItem(this.active); + this.host.toggleSelect(this.active); } } diff --git a/src/components/combo/operations/filter.ts b/src/components/combo/operations/filter.ts new file mode 100644 index 000000000..9b989e479 --- /dev/null +++ b/src/components/combo/operations/filter.ts @@ -0,0 +1,22 @@ +import { ComboHost } from '../types.js'; + +export default class FilterDataOperation { + public apply(data: T[], host: ComboHost) { + const { searchTerm, filteringOptions } = host; + const { filterKey, caseSensitive } = filteringOptions; + + if (!searchTerm) return data; + + const term = caseSensitive ? searchTerm : searchTerm.toLocaleLowerCase(); + + return data.filter((item: T) => { + const value = filterKey + ? (item[filterKey] as any).toString() + : item.toString(); + + return caseSensitive + ? value.includes(term) + : value.toLowerCase().includes(term); + }); + } +} diff --git a/src/components/combo/operations/group.ts b/src/components/combo/operations/group.ts index d89e36168..3f55a6f0a 100644 --- a/src/components/combo/operations/group.ts +++ b/src/components/combo/operations/group.ts @@ -1,8 +1,49 @@ -import { ComboHost, ComboRecord, Keys } from '../types.js'; +import { + ComboHost, + ComboRecord, + GroupingDirection, + Keys, + Values, +} from '../types.js'; export default class GroupDataOperation { + protected orderBy = new Map( + Object.entries({ + asc: 1, + desc: -1, + }) + ); + + protected resolveValue(record: T, key: Keys) { + return record[key]; + } + + protected compareValues(first: Values, second: Values) { + if (typeof first === 'string' && typeof second === 'string') { + return first.localeCompare(second); + } + return first > second ? 1 : first < second ? -1 : 0; + } + + protected compareObjects( + first: T, + second: T, + key: Keys, + direction: GroupingDirection + ) { + const [a, b] = [ + this.resolveValue(first, key), + this.resolveValue(second, key), + ]; + + return this.orderBy.get(direction)! * this.compareValues(a, b); + } + public apply(data: T[], host: ComboHost) { - const { groupKey, valueKey, displayKey } = host; + const { groupKey, valueKey, displayKey, groupSorting } = host; + + if (!groupKey) return data; + const result = new Map(); data.forEach((item: T) => { @@ -19,6 +60,14 @@ export default class GroupDataOperation { } group.push(item); + + group.sort((a: ComboRecord, b: ComboRecord) => { + if (!a.header && !b.header) { + return this.compareObjects(a, b, displayKey!, groupSorting!); + } + return 1; + }); + result.set(key, group); }); diff --git a/src/components/combo/types.ts b/src/components/combo/types.ts index 11425f199..a33b8f4f2 100644 --- a/src/components/combo/types.ts +++ b/src/components/combo/types.ts @@ -12,3 +12,10 @@ export type ComboRecord = T & ComboRecordMeta; export type ComboHost = ReactiveControllerHost & IgcComboComponent; + +export type GroupingDirection = 'asc' | 'desc'; + +export interface FilteringOptions { + filterKey: Keys | null; + caseSensitive?: boolean; +} From 808d129cf566d4b2fe5553b31d73f4984b97a6b1 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 29 Nov 2022 15:50:26 +0200 Subject: [PATCH 20/82] feat(combo): add case sensitivity and filteringOptions --- src/components/combo/combo.ts | 23 +-- src/components/combo/controllers/data.ts | 34 ++++- src/components/combo/operations/filter.ts | 10 +- src/components/combo/operations/group.ts | 17 +-- .../combo/themes/light/combo.base.scss | 8 ++ src/components/combo/types.ts | 7 + src/components/combo/utils/converters.ts | 13 ++ src/components/icon/icon.registry.ts | 1 + stories/combo.stories.ts | 134 ++++++++++++++---- 9 files changed, 192 insertions(+), 55 deletions(-) create mode 100644 src/components/combo/utils/converters.ts diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 6346b4d7a..86f1ef3fd 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -25,6 +25,7 @@ import { import { DataController } from './controllers/data.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { partNameMap } from '../common/util.js'; +import { filteringOptionsConverter } from './utils/converters.js'; defineComponents( IgcComboListComponent, @@ -101,9 +102,13 @@ export default class IgcComboComponent public groupKey?: Keys = this.displayKey; @property({ attribute: 'group-sorting', reflect: false }) - public groupSorting?: GroupingDirection = 'asc'; + public groupSorting: GroupingDirection = 'asc'; - @property({ attribute: 'filtering-options', reflect: false }) + @property({ + attribute: 'filtering-options', + reflect: false, + converter: filteringOptionsConverter, + }) public filteringOptions: FilteringOptions = { filterKey: this.displayKey ?? null, caseSensitive: false, @@ -115,9 +120,6 @@ export default class IgcComboComponent @property({ type: Boolean, attribute: 'disable-filtering', reflect: false }) public disableFiltering = false; - @state() - public searchTerm = ''; - @state() public dataState: Array> = []; @@ -127,6 +129,10 @@ export default class IgcComboComponent @watch('data') protected dataChanged() { this.dataState = structuredClone(this.data); + + if (this.hasUpdated) { + this.pipeline(); + } } @watch('valueKey') @@ -135,7 +141,7 @@ export default class IgcComboComponent } @watch('groupKey') - @watch('searchTerm') + @watch('pipeline') protected pipeline() { this.dataState = this.dataController.apply([...this.data]); this.navigationController.active = 0; @@ -266,7 +272,7 @@ export default class IgcComboComponent } protected handleSearchInput(e: CustomEvent) { - this.searchTerm = e.detail; + this.dataController.searchTerm = e.detail; } public show() { @@ -341,6 +347,7 @@ export default class IgcComboComponent protected toggleCaseSensitivity() { this.filteringOptions.caseSensitive = !this.filteringOptions.caseSensitive; + this.requestUpdate(); } public override render() { @@ -390,7 +397,7 @@ export default class IgcComboComponent > implements ReactiveController { protected grouping = new GroupDataOperation(); protected filtering = new FilterDataOperation(); + private _searchTerm = ''; constructor(protected host: ComboHost) { this.host.addController(this); } + public set searchTerm(value: string) { + this._searchTerm = value; + this.host.requestUpdate('pipeline'); + } + + public get searchTerm() { + return this._searchTerm; + } + + public get filteringOptions(): FilteringOptions { + return this.host.filteringOptions; + } + + public get groupingOptions(): GroupingOptions { + return { + valueKey: this.host.valueKey, + displayKey: this.host.displayKey, + groupKey: this.host.groupKey, + direction: this.host.groupSorting, + }; + } + public hostConnected() {} public apply(data: T[]) { - data = this.filtering.apply(data, this.host); - data = this.grouping.apply(data, this.host); + data = this.filtering.apply(data, this); + data = this.grouping.apply(data, this); return data as ComboRecord[]; } diff --git a/src/components/combo/operations/filter.ts b/src/components/combo/operations/filter.ts index 9b989e479..778337130 100644 --- a/src/components/combo/operations/filter.ts +++ b/src/components/combo/operations/filter.ts @@ -1,9 +1,11 @@ -import { ComboHost } from '../types.js'; +import { DataController } from '../controllers/data'; export default class FilterDataOperation { - public apply(data: T[], host: ComboHost) { - const { searchTerm, filteringOptions } = host; - const { filterKey, caseSensitive } = filteringOptions; + public apply(data: T[], controller: DataController) { + const { + searchTerm, + filteringOptions: { filterKey, caseSensitive }, + } = controller; if (!searchTerm) return data; diff --git a/src/components/combo/operations/group.ts b/src/components/combo/operations/group.ts index 3f55a6f0a..0fbb81321 100644 --- a/src/components/combo/operations/group.ts +++ b/src/components/combo/operations/group.ts @@ -1,10 +1,5 @@ -import { - ComboHost, - ComboRecord, - GroupingDirection, - Keys, - Values, -} from '../types.js'; +import { DataController } from '../controllers/data.js'; +import { ComboRecord, GroupingDirection, Keys, Values } from '../types.js'; export default class GroupDataOperation { protected orderBy = new Map( @@ -39,8 +34,10 @@ export default class GroupDataOperation { return this.orderBy.get(direction)! * this.compareValues(a, b); } - public apply(data: T[], host: ComboHost) { - const { groupKey, valueKey, displayKey, groupSorting } = host; + public apply(data: T[], controller: DataController) { + const { + groupingOptions: { groupKey, valueKey, displayKey, direction }, + } = controller; if (!groupKey) return data; @@ -63,7 +60,7 @@ export default class GroupDataOperation { group.sort((a: ComboRecord, b: ComboRecord) => { if (!a.header && !b.header) { - return this.compareObjects(a, b, displayKey!, groupSorting!); + return this.compareObjects(a, b, displayKey!, direction!); } return 1; }); diff --git a/src/components/combo/themes/light/combo.base.scss b/src/components/combo/themes/light/combo.base.scss index dc2651420..c266f8691 100644 --- a/src/components/combo/themes/light/combo.base.scss +++ b/src/components/combo/themes/light/combo.base.scss @@ -57,3 +57,11 @@ igc-input::part(input) { width: 100%; } } + +[part='case-icon'] { + color: color(gray, 600); +} + +[part='case-icon active'] { + color: color(gray, 900); +} diff --git a/src/components/combo/types.ts b/src/components/combo/types.ts index a33b8f4f2..78b65b9b5 100644 --- a/src/components/combo/types.ts +++ b/src/components/combo/types.ts @@ -19,3 +19,10 @@ export interface FilteringOptions { filterKey: Keys | null; caseSensitive?: boolean; } + +export interface GroupingOptions { + groupKey?: Keys; + valueKey?: Keys; + displayKey?: Keys; + direction: GroupingDirection; +} diff --git a/src/components/combo/utils/converters.ts b/src/components/combo/utils/converters.ts new file mode 100644 index 000000000..6fb5f716b --- /dev/null +++ b/src/components/combo/utils/converters.ts @@ -0,0 +1,13 @@ +import { ComplexAttributeConverter } from 'lit'; +import { FilteringOptions } from '../types.js'; + +export const filteringOptionsConverter: ComplexAttributeConverter< + FilteringOptions +> = { + toAttribute: (value: FilteringOptions) => { + return JSON.stringify(value); + }, + fromAttribute: (value: string) => { + return JSON.parse(value.replace(/'/gi, '"')) as FilteringOptions; + }, +}; diff --git a/src/components/icon/icon.registry.ts b/src/components/icon/icon.registry.ts index 549cf8fd0..245df1a44 100644 --- a/src/components/icon/icon.registry.ts +++ b/src/components/icon/icon.registry.ts @@ -101,4 +101,5 @@ const internalIcons: IconCollection = { chip_select: ``, star: ``, star_border: ``, + case_sensitive: ``, }; diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index e82d23b5e..ae4dae79a 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -2,7 +2,7 @@ import { html } from 'lit'; import { Context, Story } from './story.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { defineAllComponents } from '../src/index.js'; -import { faker } from '@faker-js/faker'; +// import { faker } from '@faker-js/faker'; defineAllComponents(); @@ -21,6 +21,16 @@ const metadata = { description: 'The name attribute of the control.', control: 'text', }, + caseSensitiveIcon: { + type: 'boolean', + control: 'boolean', + defaultValue: false, + }, + disableFiltering: { + type: 'boolean', + control: 'boolean', + defaultValue: false, + }, scrollStrategy: { type: '"scroll" | "block" | "close" | undefined', options: ['scroll', 'block', 'close', 'undefined'], @@ -70,6 +80,8 @@ export default metadata; interface ArgTypes { value: string | undefined; name: string; + caseSensitiveIcon: boolean; + disableFiltering: boolean; scrollStrategy: 'scroll' | 'block' | 'close' | undefined; keepOpenOnOutsideClick: boolean | undefined; open: boolean; @@ -80,36 +92,36 @@ interface ArgTypes { } // endregion -interface City { - id: string; - name: string; - zip: string; - country: string; -} +// interface City { +// id: string; +// name: string; +// zip: string; +// country: string; +// } -function generateCity(): City { - const id = faker.datatype.uuid(); - const name = faker.address.cityName(); - const zip = faker.address.zipCode(); - const country = faker.address.country(); +// function generateCity(): City { +// const id = faker.datatype.uuid(); +// const name = faker.address.cityName(); +// const zip = faker.address.zipCode(); +// const country = faker.address.country(); - return { - id, - name, - zip, - country, - }; -} +// return { +// id, +// name, +// zip, +// country, +// }; +// } -function generateCities(amount = 200) { - const result: Array = []; +// function generateCities(amount = 200) { +// const result: Array = []; - for (let i = 0; i <= amount; i++) { - result.push(generateCity()); - } +// for (let i = 0; i <= amount; i++) { +// result.push(generateCity()); +// } - return result; -} +// return result; +// } // const itemTemplate = (item: City) => { // return html` @@ -125,19 +137,81 @@ function generateCities(amount = 200) { // .itemTemplate=${itemTemplate} // .headerItemTemplate=${headerItemTemplate} -const cities = generateCities(200); +// const cities = generateCities(200); +const cities = [ + { + id: 'BG01', + name: 'Sofia', + country: 'Bulgaria', + zip: 1000, + }, + { + id: 'BG02', + name: 'Plovdiv', + country: 'Bulgaria', + zip: 4000, + }, + { + id: 'BG03', + name: 'Varna', + country: 'Bulgaria', + zip: 9000, + }, + { + id: 'US01', + name: 'New York', + country: 'United States', + zip: 1000, + }, + { + id: 'US02', + name: 'Boston', + country: 'United States', + zip: 4000, + }, + { + id: 'US03', + name: 'San Francisco', + country: 'United States', + zip: 9000, + }, + { + id: 'JP01', + name: 'Tokyo', + country: 'Japan', + zip: 1000, + }, + { + id: 'JP02', + name: 'Yokohama', + country: 'Japan', + zip: 4000, + }, + { + id: 'JP03', + name: 'Osaka', + country: 'Japan', + zip: 9000, + }, +]; + +// const simpleCities = ['Sofia', 4, 'Varna', 'varna', false, { a: 1, b: 2 }, -1, true]; + const Template: Story = ( - { name }: ArgTypes, + { name, disableFiltering, caseSensitiveIcon }: ArgTypes, { globals: { direction } }: Context ) => html`
This is a custom header From 822a2fb5280b3a5dd6f2f5e85d815a8223bbdc34 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 29 Nov 2022 16:30:24 +0200 Subject: [PATCH 21/82] refactor(combo): update FilteringOptions --- src/components/combo/combo.ts | 2 +- src/components/combo/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 86f1ef3fd..efff59491 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -110,7 +110,7 @@ export default class IgcComboComponent converter: filteringOptionsConverter, }) public filteringOptions: FilteringOptions = { - filterKey: this.displayKey ?? null, + filterKey: this.displayKey, caseSensitive: false, }; diff --git a/src/components/combo/types.ts b/src/components/combo/types.ts index 78b65b9b5..55fa0f8be 100644 --- a/src/components/combo/types.ts +++ b/src/components/combo/types.ts @@ -16,7 +16,7 @@ export type ComboHost = ReactiveControllerHost & export type GroupingDirection = 'asc' | 'desc'; export interface FilteringOptions { - filterKey: Keys | null; + filterKey: Keys | undefined; caseSensitive?: boolean; } From c592cafcf8ebb50865a48201518668efaf3d0502 Mon Sep 17 00:00:00 2001 From: Marin Popov Date: Tue, 29 Nov 2022 16:31:31 +0200 Subject: [PATCH 22/82] style(combo): Make search field to look kind of the same as in material but keep the unique feel of the theme (#565) Co-authored-by: Simeon Simeonoff --- src/components/combo/combo.ts | 2 ++ .../combo/themes/light/combo.base.scss | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index efff59491..b41d54ec3 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -353,6 +353,7 @@ export default class IgcComboComponent public override render() { return html` >
e.stopPropagation()} diff --git a/src/components/combo/themes/light/combo.base.scss b/src/components/combo/themes/light/combo.base.scss index c266f8691..aff921d99 100644 --- a/src/components/combo/themes/light/combo.base.scss +++ b/src/components/combo/themes/light/combo.base.scss @@ -17,6 +17,10 @@ igc-input::part(input) { } } +// igc-input::part(container) { +// --input-background: transparent; +// } + :host([disabled]) { ::slotted([slot='helper-text']) { color: color(gray, 400); @@ -40,6 +44,27 @@ igc-input::part(input) { igc-input { --ig-size: var(--ig-size-small); } + + igc-input::part(container) { + background: transparent; + border-inline-start: 0; + border-inline-end: 0; + border-block-start: 0; + border-radius: 0; + } + + igc-input::part(start) { + display: none; + } + + igc-input::part(input) { + border-radius: 0; + padding: 0; + border-inline-start: 0; + border-inline-end: 0; + border-block-start: 0; + box-shadow: none; + } } [part='filter-input'] { From 5a60c5341872e3e957a77e5b72c24c4fe1358c70 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 29 Nov 2022 17:49:10 +0200 Subject: [PATCH 23/82] fix(combo): lint and filtering --- src/components/combo/combo.ts | 25 +++++++++++-------- .../combo/themes/light/combo.base.scss | 1 + .../themes/light/item/combo-item.base.scss | 2 ++ stories/combo.stories.ts | 1 - 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index b41d54ec3..7ab2a6f35 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -140,6 +140,12 @@ export default class IgcComboComponent this.displayKey = this.displayKey ?? this.valueKey; } + @watch('displayKey') + protected updateFilterKey() { + this.filteringOptions.filterKey = + this.filteringOptions.filterKey ?? this.displayKey; + } + @watch('groupKey') @watch('pipeline') protected pipeline() { @@ -163,7 +169,7 @@ export default class IgcComboComponent } @property({ attribute: false }) - public itemTemplate: (item: T) => TemplateResult = (item) => { + public itemTemplate: (item: ComboRecord) => TemplateResult = (item) => { if (this.displayKey) { return html`${item[this.displayKey]}`; } @@ -173,7 +179,7 @@ export default class IgcComboComponent @property({ attribute: false }) public headerItemTemplate: (item: ComboRecord) => TemplateResult = ( - item + item: ComboRecord ) => { return html`${item[this.groupKey!]}`; }; @@ -289,12 +295,11 @@ export default class IgcComboComponent this.open ? this.hide() : this.show(); } - protected itemRenderer = ( - item: ComboRecord, - index: number - ): TemplateResult => { + protected itemRenderer = (item: T, index: number): TemplateResult => { + const record = item as ComboRecord; + const headerTemplate = html`${this.headerItemTemplate(item)}${this.headerItemTemplate(record)}`; const itemTemplate = html` .index=${index} .active=${this.navigationController.active === index} .selected=${this.selected.has(item)} - >${this.itemTemplate(item)}${this.itemTemplate(record)}`; - return html`${item?.header ? headerTemplate : itemTemplate}`; + return html`${record.header ? headerTemplate : itemTemplate}`; }; protected keydownHandler(event: KeyboardEvent) { @@ -357,7 +362,7 @@ export default class IgcComboComponent part="target" exportparts="container: input, input: native-input, label, prefix, suffix" @click=${this.toggle} - .value=${ifDefined(this.value)} + value=${ifDefined(this.value)} readonly > = ( value-key="id" display-key="name" group-key="country" - filtering-options="{'filterKey': 'name', 'caseSensitive': false}" ?case-sensitive-icon=${ifDefined(caseSensitiveIcon)} ?disable-filtering=${ifDefined(disableFiltering)} group-sorting="asc" From 75b4b973aef5cc2d29c8d4230c959e02210d6474 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 29 Nov 2022 20:05:43 +0200 Subject: [PATCH 24/82] refactor(combo): add properties and templates --- src/components/combo/combo.ts | 90 +++++++++++++++++- .../combo/themes/light/combo.base.scss | 6 +- stories/combo.stories.ts | 93 +++++++++++++++++-- 3 files changed, 176 insertions(+), 13 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 7ab2a6f35..1f5486ca9 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -5,20 +5,26 @@ import { styles as bootstrap } from './themes/light/combo.bootstrap.css.js'; import { styles as material } from './themes/light/combo.material.css.js'; import { styles as fluent } from './themes/light/combo.fluent.css.js'; import { styles as indigo } from './themes/light/combo.indigo.css.js'; -import { property, query, state } from 'lit/decorators.js'; +import { + property, + query, + queryAssignedElements, + state, +} from 'lit/decorators.js'; import { watch } from '../common/decorators/watch.js'; import { defineComponents } from '../common/definitions/defineComponents.js'; import IgcComboListComponent from './combo-list.js'; import IgcComboItemComponent from './combo-item.js'; import IgcComboHeaderComponent from './combo-header.js'; import IgcInputComponent from '../input/input.js'; +import IgcIconComponent from '../icon/icon.js'; import { NavigationController } from './controllers/navigation.js'; import { IgcToggleController } from '../toggle/toggle.controller.js'; import { IgcToggleComponent } from '../toggle/types.js'; import { Keys, - ComboRecord, Values, + ComboRecord, GroupingDirection, FilteringOptions, } from './types.js'; @@ -28,6 +34,7 @@ import { partNameMap } from '../common/util.js'; import { filteringOptionsConverter } from './utils/converters.js'; defineComponents( + IgcIconComponent, IgcComboListComponent, IgcComboItemComponent, IgcComboHeaderComponent, @@ -76,6 +83,15 @@ export default class IgcComboComponent @query('[part="target"]') private target!: HTMLElement; + @queryAssignedElements({ slot: 'helper-text' }) + protected helperText!: Array; + + @queryAssignedElements({ slot: 'suffix' }) + protected inputSuffix!: Array; + + @queryAssignedElements({ slot: 'prefix' }) + protected inputPrefix!: Array; + /** The data source used to build the list of options. */ @property({ attribute: false }) public data: Array = []; @@ -88,6 +104,38 @@ export default class IgcComboComponent @property() public name!: string; + /** The disabled attribute of the control. */ + @property({ reflect: true, type: Boolean }) + public disabled = false; + + /** The required attribute of the control. */ + @property({ reflect: true, type: Boolean }) + public required = false; + + /** The invalid attribute of the control. */ + @property({ reflect: true, type: Boolean }) + public invalid = false; + + /** The outlined attribute of the control. */ + @property({ reflect: true, type: Boolean }) + public outlined = false; + + /** The autofocus attribute of the control. */ + @property({ type: Boolean }) + public override autofocus!: boolean; + + /** The label attribute of the control. */ + @property({ type: String }) + public label!: string; + + /** The placeholder attribute of the control. */ + @property({ type: String }) + public placeholder!: string; + + /** The direction attribute of the control. */ + @property({ reflect: true }) + public override dir: 'ltr' | 'rtl' | 'auto' = 'auto'; + /** Sets the open state of the component. */ @property({ type: Boolean }) public open = false; @@ -193,6 +241,12 @@ export default class IgcComboComponent }); } + protected override firstUpdated() { + if (this.autofocus) { + this.target.focus(); + } + } + private selectValueKeys(values: Values[]) { if (values.length === 0) return; @@ -352,19 +406,36 @@ export default class IgcComboComponent protected toggleCaseSensitivity() { this.filteringOptions.caseSensitive = !this.filteringOptions.caseSensitive; - this.requestUpdate(); + this.requestUpdate('pipeline'); + } + + protected get hasPrefixes() { + return this.inputPrefix.length > 0; + } + + protected get hasSuffixes() { + return this.inputSuffix.length > 0; } public override render() { return html` + + + > + + + exportparts="container: input, input: native-input, label, prefix, suffix" @igcInput=${this.handleSearchInput} @keydown=${(e: KeyboardEvent) => e.stopPropagation()} + dir=${this.dir} >
+
+ +
`; } } diff --git a/src/components/combo/themes/light/combo.base.scss b/src/components/combo/themes/light/combo.base.scss index 176409b7a..33c2fa7e2 100644 --- a/src/components/combo/themes/light/combo.base.scss +++ b/src/components/combo/themes/light/combo.base.scss @@ -38,8 +38,9 @@ igc-input::part(input) { background: color(surface); color: color(gray, 700); - padding: rem(8px) 0; box-shadow: elevation(8); + overflow: hidden; + outline-style: none; igc-input { --ig-size: var(--ig-size-small); @@ -77,7 +78,8 @@ igc-input::part(input) { $min-width: rem(150px); min-width: $min-width; - min-height: calc($min-width * 3); + min-height: rem(200px) !important; + outline-style: none; > * { width: 100%; diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 6c67e94cb..4f5275e88 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -2,6 +2,7 @@ import { html } from 'lit'; import { Context, Story } from './story.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { defineAllComponents } from '../src/index.js'; +import { registerIconFromText } from '../src/components/icon/icon.registry'; // import { faker } from '@faker-js/faker'; defineAllComponents(); @@ -21,6 +22,54 @@ const metadata = { description: 'The name attribute of the control.', control: 'text', }, + disabled: { + type: 'boolean', + description: 'The disabled attribute of the control.', + control: 'boolean', + defaultValue: false, + }, + required: { + type: 'boolean', + description: 'The required attribute of the control.', + control: 'boolean', + defaultValue: false, + }, + invalid: { + type: 'boolean', + description: 'The invalid attribute of the control.', + control: 'boolean', + defaultValue: false, + }, + outlined: { + type: 'boolean', + description: 'The outlined attribute of the control.', + control: 'boolean', + defaultValue: false, + }, + autofocus: { + type: 'boolean', + description: 'The autofocus attribute of the control.', + control: 'boolean', + }, + label: { + type: 'string', + description: 'The label attribute of the control.', + control: 'text', + }, + placeholder: { + type: 'string', + description: 'The placeholder attribute of the control.', + control: 'text', + }, + dir: { + type: '"ltr" | "rtl" | "auto"', + description: 'The direction attribute of the control.', + options: ['ltr', 'rtl', 'auto'], + control: { + type: 'inline-radio', + }, + defaultValue: 'auto', + }, caseSensitiveIcon: { type: 'boolean', control: 'boolean', @@ -80,6 +129,14 @@ export default metadata; interface ArgTypes { value: string | undefined; name: string; + disabled: boolean; + required: boolean; + invalid: boolean; + outlined: boolean; + autofocus: boolean; + label: string; + placeholder: string; + dir: 'ltr' | 'rtl' | 'auto'; caseSensitiveIcon: boolean; disableFiltering: boolean; scrollStrategy: 'scroll' | 'block' | 'close' | undefined; @@ -197,13 +254,32 @@ const cities = [ // const simpleCities = ['Sofia', 4, 'Varna', 'varna', false, { a: 1, b: 2 }, -1, true]; +registerIconFromText( + 'location', + '' +); + const Template: Story = ( - { name, disableFiltering, caseSensitiveIcon }: ArgTypes, + { + name, + disableFiltering, + caseSensitiveIcon, + label = 'Location(s)', + placeholder = 'Cities of interest', + open = false, + disabled = false, + outlined = false, + invalid = false, + required = false, + autofocus = false, + }: ArgTypes, { globals: { direction } }: Context ) => html` = ( ?case-sensitive-icon=${ifDefined(caseSensitiveIcon)} ?disable-filtering=${ifDefined(disableFiltering)} group-sorting="asc" + ?open=${open} + ?autofocus=${autofocus} + ?outlined=${outlined} + ?required=${required} + ?disabled=${disabled} + ?invalid=${invalid} + .dir=${direction} > -
- This is a custom header -
-
- This is a custom footer -
+ + Sample helper text.
`; From 57a1b97ea0936002805f45bfc999d943684974df Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 29 Nov 2022 20:10:59 +0200 Subject: [PATCH 25/82] refactor(combo): grouping and filtering --- src/components/combo/operations/filter.ts | 2 +- src/components/combo/operations/group.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/combo/operations/filter.ts b/src/components/combo/operations/filter.ts index 778337130..220c4cb2e 100644 --- a/src/components/combo/operations/filter.ts +++ b/src/components/combo/operations/filter.ts @@ -18,7 +18,7 @@ export default class FilterDataOperation { return caseSensitive ? value.includes(term) - : value.toLowerCase().includes(term); + : value.toLocaleLowerCase().includes(term); }); } } diff --git a/src/components/combo/operations/group.ts b/src/components/combo/operations/group.ts index 0fbb81321..0212478a2 100644 --- a/src/components/combo/operations/group.ts +++ b/src/components/combo/operations/group.ts @@ -60,7 +60,7 @@ export default class GroupDataOperation { group.sort((a: ComboRecord, b: ComboRecord) => { if (!a.header && !b.header) { - return this.compareObjects(a, b, displayKey!, direction!); + return this.compareObjects(a, b, displayKey!, direction); } return 1; }); From 9c17eb37eb981fa46c1ae617db9d0a8d87bd2f35 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 29 Nov 2022 20:16:49 +0200 Subject: [PATCH 26/82] fix(combo): autofocus --- src/components/combo/combo.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 1f5486ca9..2cae44ba3 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -241,12 +241,6 @@ export default class IgcComboComponent }); } - protected override firstUpdated() { - if (this.autofocus) { - this.target.focus(); - } - } - private selectValueKeys(values: Values[]) { if (values.length === 0) return; @@ -431,6 +425,7 @@ export default class IgcComboComponent .required=${this.required} .invalid=${this.invalid} .outlined=${this.outlined} + .autofocus=${this.autofocus} readonly > From 37968f4fe15a88998d0ea8b9a71118f52e3db9f7 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 29 Nov 2022 23:09:16 +0200 Subject: [PATCH 27/82] refactor(combo): focus input and empty template --- src/components/combo/combo.ts | 22 ++++++++++++++----- .../combo/controllers/navigation.ts | 4 +++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 2cae44ba3..f50265f59 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -80,9 +80,6 @@ export default class IgcComboComponent protected dataController = new DataController(this); protected toggleController!: IgcToggleController; - @query('[part="target"]') - private target!: HTMLElement; - @queryAssignedElements({ slot: 'helper-text' }) protected helperText!: Array; @@ -92,6 +89,12 @@ export default class IgcComboComponent @queryAssignedElements({ slot: 'prefix' }) protected inputPrefix!: Array; + @query('[part="search-input"]') + private input!: IgcInputComponent; + + @query('[part="target"]') + private target!: IgcInputComponent; + /** The data source used to build the list of options. */ @property({ attribute: false }) public data: Array = []; @@ -329,9 +332,12 @@ export default class IgcComboComponent this.dataController.searchTerm = e.detail; } - public show() { + public async show() { if (this.open) return; this.open = true; + + await this.updateComplete; + this.input.focus(); } public hide() { @@ -376,6 +382,7 @@ export default class IgcComboComponent protected itemClickHandler(event: MouseEvent) { const target = event.target as IgcComboItemComponent; this.toggleSelect(target.index); + this.input.focus(); } public toggleSelect(index: number) { @@ -421,6 +428,7 @@ export default class IgcComboComponent placeholder=${ifDefined(this.placeholder)} label=${ifDefined(this.label)} dir=${this.dir} + @keydown=${(e: KeyboardEvent) => e.stopPropagation()} .disabled="${this.disabled}" .required=${this.required} .invalid=${this.invalid} @@ -460,12 +468,12 @@ export default class IgcComboComponent
part="list" .items=${this.dataState} .renderItem=${this.itemRenderer} + ?hidden=${this.dataState.length === 0} > + 0}> +
The list is empty
+
} protected space() { - if (this.active !== -1) { + const item = this.host.dataState[this.active]; + + if (!item.header) { this.host.toggleSelect(this.active); } } From 1f705e015ca25f035bb839ab38b430e1bc8aa9d6 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 29 Nov 2022 23:32:05 +0200 Subject: [PATCH 28/82] refactor(combo): update template after reading content --- src/components/combo/combo.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index f50265f59..301334366 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -244,6 +244,11 @@ export default class IgcComboComponent }); } + public override async firstUpdated() { + await this.updateComplete; + this.requestUpdate(); + } + private selectValueKeys(values: Values[]) { if (values.length === 0) return; From 5db6e0a1ec66fa9ab076963963935426122b0073 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 30 Nov 2022 10:11:22 +0200 Subject: [PATCH 29/82] refactor(combo): hide on blur --- src/components/combo/combo.ts | 22 ++++++++++++++++-- .../combo/themes/light/combo.base.scss | 7 ------ stories/combo.stories.ts | 23 +++++++++++++++++-- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 301334366..6016ac27a 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -95,6 +95,9 @@ export default class IgcComboComponent @query('[part="target"]') private target!: IgcInputComponent; + @query('igc-combo-list') + private list!: IgcComboListComponent; + /** The data source used to build the list of options. */ @property({ attribute: false }) public data: Array = []; @@ -127,6 +130,10 @@ export default class IgcComboComponent @property({ type: Boolean }) public override autofocus!: boolean; + /** Focuses the first item in the list of options when the menu opens.*/ + @property({ attribute: 'autofocus-options', type: Boolean }) + public autofocusOptions = false; + /** The label attribute of the control. */ @property({ type: String }) public label!: string; @@ -135,6 +142,10 @@ export default class IgcComboComponent @property({ type: String }) public placeholder!: string; + /** The placeholder attribute of the search input. */ + @property({ attribute: 'placeholder-search', type: String }) + public placeholderSearch = 'Search'; + /** The direction attribute of the control. */ @property({ reflect: true }) public override dir: 'ltr' | 'rtl' | 'auto' = 'auto'; @@ -242,6 +253,8 @@ export default class IgcComboComponent target: this.target, closeCallback: () => {}, }); + + this.addEventListener('blur', () => this.hide()); } public override async firstUpdated() { @@ -342,7 +355,12 @@ export default class IgcComboComponent this.open = true; await this.updateComplete; - this.input.focus(); + + this.list.focus(); + + if (!this.autofocusOptions) { + this.input.focus(); + } } public hide() { @@ -479,7 +497,7 @@ export default class IgcComboComponent
e.stopPropagation()} diff --git a/src/components/combo/themes/light/combo.base.scss b/src/components/combo/themes/light/combo.base.scss index 33c2fa7e2..c4be96af6 100644 --- a/src/components/combo/themes/light/combo.base.scss +++ b/src/components/combo/themes/light/combo.base.scss @@ -75,15 +75,8 @@ igc-input::part(input) { } [part='list'] { - $min-width: rem(150px); - - min-width: $min-width; min-height: rem(200px) !important; outline-style: none; - - > * { - width: 100%; - } } [part='case-icon'] { diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 4f5275e88..cf0cbf56e 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -51,6 +51,13 @@ const metadata = { description: 'The autofocus attribute of the control.', control: 'boolean', }, + autofocusOptions: { + type: 'boolean', + description: + 'Focuses the first item in the list of options when the menu opens.', + control: 'boolean', + defaultValue: false, + }, label: { type: 'string', description: 'The label attribute of the control.', @@ -61,6 +68,12 @@ const metadata = { description: 'The placeholder attribute of the control.', control: 'text', }, + placeholderSearch: { + type: 'string', + description: 'The placeholder attribute of the search input.', + control: 'text', + defaultValue: 'Search', + }, dir: { type: '"ltr" | "rtl" | "auto"', description: 'The direction attribute of the control.', @@ -134,8 +147,10 @@ interface ArgTypes { invalid: boolean; outlined: boolean; autofocus: boolean; + autofocusOptions: boolean; label: string; placeholder: string; + placeholderSearch: string; dir: 'ltr' | 'rtl' | 'auto'; caseSensitiveIcon: boolean; disableFiltering: boolean; @@ -266,34 +281,38 @@ const Template: Story = ( caseSensitiveIcon, label = 'Location(s)', placeholder = 'Cities of interest', + placeholderSearch = 'Search', open = false, disabled = false, outlined = false, invalid = false, required = false, autofocus = false, + autofocusOptions, }: ArgTypes, { globals: { direction } }: Context ) => html` Sample helper text. From c24c294340ad7b2b739003679d33d43093d5a75d Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 30 Nov 2022 11:51:24 +0200 Subject: [PATCH 30/82] fix(combo): headers change size --- src/components/combo/combo-header.ts | 2 +- .../combo/themes/light/header/combo-header.base.scss | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 src/components/combo/themes/light/header/combo-header.base.scss diff --git a/src/components/combo/combo-header.ts b/src/components/combo/combo-header.ts index 04cd7bded..d603d8061 100644 --- a/src/components/combo/combo-header.ts +++ b/src/components/combo/combo-header.ts @@ -1,6 +1,6 @@ import { html, LitElement } from 'lit'; import { themes } from '../../theming/theming-decorator.js'; -import { styles } from '../dropdown/themes/light/dropdown-header.base.css.js'; +import { styles } from './themes/light/header/combo-header.base.css.js'; import { styles as bootstrap } from '../dropdown/themes/light/dropdown-header.bootstrap.css.js'; import { styles as fluent } from '../dropdown/themes/light/dropdown-header.fluent.css.js'; diff --git a/src/components/combo/themes/light/header/combo-header.base.scss b/src/components/combo/themes/light/header/combo-header.base.scss new file mode 100644 index 000000000..fd9f2b6da --- /dev/null +++ b/src/components/combo/themes/light/header/combo-header.base.scss @@ -0,0 +1,6 @@ +@use '../../../../../styles/utilities' as *; +@use '../../../../dropdown/themes/light/dropdown-header.base' as *; + +:host { + --component-size: var(--ig-size, var(--ig-size-medium)); +} From b7cebee15524bae5a725e28e610dcdc882b8dfa2 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 30 Nov 2022 11:53:10 +0200 Subject: [PATCH 31/82] feat(combo): add navigateTo --- src/components/combo/combo.ts | 4 ++++ src/components/combo/controllers/navigation.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 6016ac27a..70ab6e8bc 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -422,6 +422,10 @@ export default class IgcComboComponent this.navigationController.active = index; } + public navigateTo(item: T) { + this.navigationController.navigateTo(item, this.list); + } + protected handleClearIconClick(e: MouseEvent) { e.stopPropagation(); this.deselect(); diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts index 227c1f1a9..5653bcdd4 100644 --- a/src/components/combo/controllers/navigation.ts +++ b/src/components/combo/controllers/navigation.ts @@ -120,6 +120,11 @@ export class NavigationController this.active = START_INDEX; } + public navigateTo(item: T, container: IgcComboListComponent) { + this.active = this.host.dataState.findIndex((i) => i === item); + container.scrollToIndex(this.active); + } + public navigate(event: KeyboardEvent, container: IgcComboListComponent) { if (this.handlers.has(event.key)) { event.preventDefault(); From 1a1e139e06e8e523b09487ca280f665dc153110d Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 30 Nov 2022 13:15:17 +0200 Subject: [PATCH 32/82] refactor(combo): update navigation --- src/components/combo/combo.ts | 15 +++-- .../combo/controllers/navigation.ts | 56 +++++++++++++++++-- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 70ab6e8bc..88f85efbf 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -90,7 +90,7 @@ export default class IgcComboComponent protected inputPrefix!: Array; @query('[part="search-input"]') - private input!: IgcInputComponent; + public input!: IgcInputComponent; @query('[part="target"]') private target!: IgcInputComponent; @@ -255,6 +255,10 @@ export default class IgcComboComponent }); this.addEventListener('blur', () => this.hide()); + this.addEventListener( + 'keydown', + this.navigationController.navigateHost.bind(this.navigationController) + ); } public override async firstUpdated() { @@ -398,7 +402,7 @@ export default class IgcComboComponent ) as IgcComboListComponent; if (target) { - this.navigationController.navigate(event, target); + this.navigationController.navigateList(event, target); } } @@ -455,7 +459,9 @@ export default class IgcComboComponent placeholder=${ifDefined(this.placeholder)} label=${ifDefined(this.label)} dir=${this.dir} - @keydown=${(e: KeyboardEvent) => e.stopPropagation()} + @keydown=${this.navigationController.navigateHost.bind( + this.navigationController + )} .disabled="${this.disabled}" .required=${this.required} .invalid=${this.invalid} @@ -504,7 +510,8 @@ export default class IgcComboComponent placeholder=${this.placeholderSearch} exportparts="container: input, input: native-input, label, prefix, suffix" @igcInput=${this.handleSearchInput} - @keydown=${(e: KeyboardEvent) => e.stopPropagation()} + @keydown=${(e: KeyboardEvent) => + this.navigationController.navigateInput(e, this.list)} dir=${this.dir} > implements ReactiveController { - protected handlers = new Map( + protected hostHandlers = new Map( + Object.entries({ + Escape: this.escape, + ArrowDown: this.hostArrowDown, + }) + ); + + protected searchInputHandlers = new Map( + Object.entries({ + Escape: this.escape, + ArrowDown: this.inputArrowDown, + Tab: this.inputArrowDown, + }) + ); + + protected listHandlers = new Map( Object.entries({ ArrowDown: this.arrowDown, ArrowUp: this.arrowUp, ' ': this.space, Enter: this.enter, + Escape: this.escape, Home: this.home, End: this.end, }) @@ -75,11 +91,27 @@ export class NavigationController } } + protected escape() { + this.host.hide(); + } + protected enter() { this.space(); this.host.open = false; } + protected inputArrowDown(container: IgcComboListComponent) { + container.focus(); + + if (this.active === 0) { + this.active = this.firstItem; + } + } + + protected hostArrowDown() { + this.host.show(); + } + protected arrowDown(container: IgcComboListComponent) { this.getNextItem(DIRECTION.Down); container.scrollToIndex(this.active, 'center'); @@ -125,10 +157,26 @@ export class NavigationController container.scrollToIndex(this.active); } - public navigate(event: KeyboardEvent, container: IgcComboListComponent) { - if (this.handlers.has(event.key)) { + public navigateHost(event: KeyboardEvent) { + if (this.hostHandlers.has(event.key)) { + event.preventDefault(); + this.hostHandlers.get(event.key)!.call(this); + } + } + + public navigateInput(event: KeyboardEvent, container: IgcComboListComponent) { + event.stopPropagation(); + + if (this.searchInputHandlers.has(event.key)) { + event.preventDefault(); + this.searchInputHandlers.get(event.key)!.call(this, container); + } + } + + public navigateList(event: KeyboardEvent, container: IgcComboListComponent) { + if (this.listHandlers.has(event.key)) { event.preventDefault(); - this.handlers.get(event.key)!.call(this, container); + this.listHandlers.get(event.key)!.call(this, container); } } } From a5c3b783b2f9b0217164ec3d493d798e9a25466a Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 30 Nov 2022 15:30:53 +0200 Subject: [PATCH 33/82] refactor(combo): fire rudimentary events --- src/components/combo/combo.ts | 66 ++++++++++++++++--- src/components/combo/controllers/data.ts | 17 ++++- .../combo/controllers/navigation.ts | 6 +- src/components/combo/types.ts | 13 ++++ stories/combo.stories.ts | 16 +++++ 5 files changed, 104 insertions(+), 14 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 88f85efbf..9a18f4417 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -27,11 +27,14 @@ import { ComboRecord, GroupingDirection, FilteringOptions, + IgcComboEventMap, } from './types.js'; import { DataController } from './controllers/data.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { partNameMap } from '../common/util.js'; import { filteringOptionsConverter } from './utils/converters.js'; +import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; +import { Constructor } from '../common/mixins/constructor.js'; defineComponents( IgcIconComponent, @@ -70,11 +73,13 @@ defineComponents( */ @themes({ material, bootstrap, fluent, indigo }) export default class IgcComboComponent - extends LitElement + extends EventEmitterMixin>( + LitElement + ) implements IgcToggleComponent { public static readonly tagName = 'igc-combo'; - public static override styles = styles; + public static styles = styles; protected navigationController = new NavigationController(this); protected dataController = new DataController(this); @@ -210,8 +215,10 @@ export default class IgcComboComponent @watch('groupKey') @watch('pipeline') - protected pipeline() { - this.dataState = this.dataController.apply([...this.data]); + protected async pipeline() { + this.dataState = (await this.dataController.apply([ + ...this.data, + ])) as ComboRecord[]; this.navigationController.active = 0; } @@ -228,6 +235,10 @@ export default class IgcComboComponent } }) .join(', '); + + this.emitEvent('igcChange', { + detail: this.value, + }); } @property({ attribute: false }) @@ -251,10 +262,17 @@ export default class IgcComboComponent this.toggleController = new IgcToggleController(this, { target: this.target, - closeCallback: () => {}, }); - this.addEventListener('blur', () => this.hide()); + this.addEventListener('focus', () => { + this.emitEvent('igcFocus'); + }); + + this.addEventListener('blur', async () => { + await this.hide(true); + this.emitEvent('igcBlur'); + }); + this.addEventListener( 'keydown', this.navigationController.navigateHost.bind(this.navigationController) @@ -266,6 +284,16 @@ export default class IgcComboComponent this.requestUpdate(); } + /** Sets focus on the component. */ + public override focus(options?: FocusOptions) { + this.target.focus(options); + } + + /** Removes focus from the component. */ + public override blur() { + this.target.blur(); + } + private selectValueKeys(values: Values[]) { if (values.length === 0) return; @@ -354,11 +382,23 @@ export default class IgcComboComponent this.dataController.searchTerm = e.detail; } - public async show() { + protected handleOpening() { + const args = { cancelable: true }; + return this.emitEvent('igcOpening', args); + } + + protected handleClosing(): boolean { + const args = { cancelable: true }; + return this.emitEvent('igcClosing', args); + } + + public async show(emit = false) { if (this.open) return; + if (emit && !this.handleOpening()) return; this.open = true; await this.updateComplete; + emit && this.emitEvent('igcOpened'); this.list.focus(); @@ -367,13 +407,17 @@ export default class IgcComboComponent } } - public hide() { + public async hide(emit = false) { if (!this.open) return; + if (emit && !this.handleClosing()) return; this.open = false; + + await this.updateComplete; + emit && this.emitEvent('igcClosed'); } public toggle() { - this.open ? this.hide() : this.show(); + this.open ? this.hide(true) : this.show(true); } protected itemRenderer = (item: T, index: number): TemplateResult => { @@ -459,6 +503,8 @@ export default class IgcComboComponent placeholder=${ifDefined(this.placeholder)} label=${ifDefined(this.label)} dir=${this.dir} + @igcFocus=${(e: Event) => e.stopPropagation()} + @igcBlur=${(e: Event) => e.stopPropagation()} @keydown=${this.navigationController.navigateHost.bind( this.navigationController )} @@ -509,6 +555,8 @@ export default class IgcComboComponent part="search-input" placeholder=${this.placeholderSearch} exportparts="container: input, input: native-input, label, prefix, suffix" + @igcFocus=${(e: Event) => e.stopPropagation()} + @igcBlur=${(e: Event) => e.stopPropagation()} @igcInput=${this.handleSearchInput} @keydown=${(e: KeyboardEvent) => this.navigationController.navigateInput(e, this.list)} diff --git a/src/components/combo/controllers/data.ts b/src/components/combo/controllers/data.ts index 6206c61c9..0a8634003 100644 --- a/src/components/combo/controllers/data.ts +++ b/src/components/combo/controllers/data.ts @@ -13,6 +13,15 @@ export class DataController implements ReactiveController { protected filtering = new FilterDataOperation(); private _searchTerm = ''; + private emitFilteringEvent() { + const args = { cancelable: true }; + return this.host.emitEvent('igcFiltering', args); + } + + private emitFilteredEvent() { + return this.host.emitEvent('igcFiltered'); + } + constructor(protected host: ComboHost) { this.host.addController(this); } @@ -41,10 +50,14 @@ export class DataController implements ReactiveController { public hostConnected() {} - public apply(data: T[]) { + public async apply(data: T[]) { + if (!this.emitFilteringEvent()) return; data = this.filtering.apply(data, this); - data = this.grouping.apply(data, this); + await this.host.updateComplete; + + this.emitFilteredEvent(); + data = this.grouping.apply(data, this); return data as ComboRecord[]; } } diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts index dbad81f9e..0a691f03a 100644 --- a/src/components/combo/controllers/navigation.ts +++ b/src/components/combo/controllers/navigation.ts @@ -92,12 +92,12 @@ export class NavigationController } protected escape() { - this.host.hide(); + this.host.hide(true); } protected enter() { this.space(); - this.host.open = false; + this.host.hide(true); } protected inputArrowDown(container: IgcComboListComponent) { @@ -109,7 +109,7 @@ export class NavigationController } protected hostArrowDown() { - this.host.show(); + this.host.show(true); } protected arrowDown(container: IgcComboListComponent) { diff --git a/src/components/combo/types.ts b/src/components/combo/types.ts index 55fa0f8be..75070ed7f 100644 --- a/src/components/combo/types.ts +++ b/src/components/combo/types.ts @@ -26,3 +26,16 @@ export interface GroupingOptions { displayKey?: Keys; direction: GroupingDirection; } + +export interface IgcComboEventMap { + /* blazorSuppress */ + igcChange: CustomEvent; + igcFocus: CustomEvent; + igcBlur: CustomEvent; + igcOpening: CustomEvent; + igcOpened: CustomEvent; + igcClosing: CustomEvent; + igcClosed: CustomEvent; + igcFiltering: CustomEvent; + igcFiltered: CustomEvent; +} diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index cf0cbf56e..064c9bb1e 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -164,6 +164,22 @@ interface ArgTypes { } // endregion +(metadata as any).parameters = { + actions: { + handles: [ + 'igcFocus', + 'igcBlur', + 'igcChange', + 'igcOpening', + 'igcOpened', + 'igcClosing', + 'igcClosed', + 'igcFiltering', + 'igcFiltered', + ], + }, +}; + // interface City { // id: string; // name: string; From e717777943bc3eb6154fa9a112004256e28a213f Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 30 Nov 2022 23:20:56 +0200 Subject: [PATCH 34/82] refactor(combo): add selection controller --- src/components/combo/combo.ts | 147 +++------------ src/components/combo/controllers/data.ts | 17 +- .../combo/controllers/navigation.ts | 8 +- src/components/combo/controllers/selection.ts | 172 ++++++++++++++++++ src/components/combo/types.ts | 19 +- stories/combo.stories.ts | 15 +- stories/stepper.stories.ts | 6 +- 7 files changed, 223 insertions(+), 161 deletions(-) create mode 100644 src/components/combo/controllers/selection.ts diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 9a18f4417..885463f58 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -19,7 +19,9 @@ import IgcComboHeaderComponent from './combo-header.js'; import IgcInputComponent from '../input/input.js'; import IgcIconComponent from '../icon/icon.js'; import { NavigationController } from './controllers/navigation.js'; +import { SelectionController } from './controllers/selection.js'; import { IgcToggleController } from '../toggle/toggle.controller.js'; +import { DataController } from './controllers/data.js'; import { IgcToggleComponent } from '../toggle/types.js'; import { Keys, @@ -29,7 +31,6 @@ import { FilteringOptions, IgcComboEventMap, } from './types.js'; -import { DataController } from './controllers/data.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { partNameMap } from '../common/util.js'; import { filteringOptionsConverter } from './utils/converters.js'; @@ -57,7 +58,7 @@ defineComponents( * * @fires igcFocus - Emitted when the select gains focus. * @fires igcBlur - Emitted when the select loses focus. - * @fires igcChange - Emitted when the control's checked state changes. + * @fires igcChange - Emitted when the control's selection has changed. * @fires igcOpening - Emitted just before the list of options is opened. * @fires igcOpened - Emitted after the list of options is opened. * @fires igcClosing - Emitter just before the list of options is closed. @@ -81,7 +82,10 @@ export default class IgcComboComponent public static readonly tagName = 'igc-combo'; public static styles = styles; + private _value?: string | undefined; + protected navigationController = new NavigationController(this); + protected selectionController = new SelectionController(this); protected dataController = new DataController(this); protected toggleController!: IgcToggleController; @@ -107,10 +111,6 @@ export default class IgcComboComponent @property({ attribute: false }) public data: Array = []; - /** The value attribute of the control. */ - @property({ type: String, reflect: false }) - public value?: string | undefined; - /** The name attribute of the control. */ @property() public name!: string; @@ -190,9 +190,6 @@ export default class IgcComboComponent @state() public dataState: Array> = []; - @state() - protected selected: Set = new Set(); - @watch('data') protected dataChanged() { this.dataState = structuredClone(this.data); @@ -216,29 +213,15 @@ export default class IgcComboComponent @watch('groupKey') @watch('pipeline') protected async pipeline() { - this.dataState = (await this.dataController.apply([ - ...this.data, - ])) as ComboRecord[]; + this.dataState = await this.dataController.apply([...this.data]); this.navigationController.active = 0; } @watch('selected', { waitUntilFirstUpdate: true }) protected updateValue() { - const values = Array.from(this.selected.values()); - - this.value = values - .map((value) => { - if (typeof value === 'object') { - return this.displayKey ? value[this.displayKey] : value; - } else { - return value; - } - }) - .join(', '); - - this.emitEvent('igcChange', { - detail: this.value, - }); + const { selected } = this.selectionController; + const values = Array.from(selected.values()); + this._value = this.selectionController.getValue(values); } @property({ attribute: false }) @@ -279,6 +262,10 @@ export default class IgcComboComponent ); } + public get value() { + return this._value; + } + public override async firstUpdated() { await this.updateComplete; this.requestUpdate(); @@ -294,88 +281,12 @@ export default class IgcComboComponent this.target.blur(); } - private selectValueKeys(values: Values[]) { - if (values.length === 0) return; - - values.forEach((value) => { - const item = this.dataState.find((i) => i[this.valueKey!] === value); - - if (item) { - this.selected.add(item); - } - }); - } - - private deselectValueKeys(values: Values[]) { - if (values.length === 0) return; - - values.forEach((value) => { - const item = this.dataState.find((i) => i[this.valueKey!] === value); - - if (item) { - this.selected.delete(item); - } - }); - } - - private selectObjects(items: T[]) { - if (items.length === 0) return; - - items.forEach((item) => { - this.selected.add(item as ComboRecord); - }); - } - - private deselectObjects(items: T[]) { - if (items.length === 0) return; - - items.forEach((item) => { - this.selected.delete(item as ComboRecord); - }); - } - - private selectAll() { - this.dataState - .filter((i) => !i.header) - .forEach((item) => { - this.selected.add(item); - }); - this.requestUpdate('selected'); + public select(items?: T[] | Values[], emit = false) { + this.selectionController.select(items, emit); } - private deselectAll() { - this.selected.clear(); - this.requestUpdate('selected'); - } - - public select(items?: T[] | Values[]) { - if (!items || items.length === 0) { - this.selectAll(); - return; - } - - if (this.valueKey) { - this.selectValueKeys(items as Values[]); - } else { - this.selectObjects(items as T[]); - } - - this.requestUpdate('selected'); - } - - public deselect(items?: T[] | Values[]) { - if (!items || items.length === 0) { - this.deselectAll(); - return; - } - - if (this.valueKey) { - this.deselectValueKeys(items as Values[]); - } else { - this.deselectObjects(items as T[]); - } - - this.requestUpdate('selected'); + public deselect(items?: T[] | Values[], emit = false) { + this.selectionController.deselect(items, emit); } protected handleSearchInput(e: CustomEvent) { @@ -422,6 +333,7 @@ export default class IgcComboComponent protected itemRenderer = (item: T, index: number): TemplateResult => { const record = item as ComboRecord; + const { selected } = this.selectionController; const headerTemplate = html`${this.headerItemTemplate(record)} @click=${this.itemClickHandler.bind(this)} .index=${index} .active=${this.navigationController.active === index} - .selected=${this.selected.has(item)} + .selected=${selected.has(item)} >${this.itemTemplate(record)}`; @@ -457,20 +369,11 @@ export default class IgcComboComponent } public toggleSelect(index: number) { - const item = this.dataState[index]; - - if (this.valueKey) { - !this.selected.has(item) - ? this.select([item[this.valueKey]]) - : this.deselect([item[this.valueKey]]); - } else { - !this.selected.has(item) ? this.select([item]) : this.deselect([item]); - } - + this.selectionController.changeSelection(index); this.navigationController.active = index; } - public navigateTo(item: T) { + protected navigateTo(item: T) { this.navigationController.navigateTo(item, this.list); } @@ -494,12 +397,14 @@ export default class IgcComboComponent } public override render() { + const { selected } = this.selectionController; + return html` slot="suffix" part="clear-icon" @click=${this.handleClearIconClick} - ?hidden=${this.selected.size === 0} + ?hidden=${selected.size === 0} > implements ReactiveController { protected filtering = new FilterDataOperation(); private _searchTerm = ''; - private emitFilteringEvent() { - const args = { cancelable: true }; - return this.host.emitEvent('igcFiltering', args); - } - - private emitFilteredEvent() { - return this.host.emitEvent('igcFiltered'); - } - constructor(protected host: ComboHost) { this.host.addController(this); } @@ -50,14 +41,10 @@ export class DataController implements ReactiveController { public hostConnected() {} - public async apply(data: T[]) { - if (!this.emitFilteringEvent()) return; + public async apply(data: T[]): Promise[]> { data = this.filtering.apply(data, this); - await this.host.updateComplete; - - this.emitFilteredEvent(); - data = this.grouping.apply(data, this); + return data as ComboRecord[]; } } diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts index 0a691f03a..68776947d 100644 --- a/src/components/combo/controllers/navigation.ts +++ b/src/components/combo/controllers/navigation.ts @@ -1,10 +1,6 @@ -import { ReactiveController, ReactiveControllerHost } from 'lit'; +import { ReactiveController } from 'lit'; import IgcComboListComponent from '../combo-list.js'; -import IgcComboComponent from '../combo.js'; -import { ComboRecord } from '../types.js'; - -type ComboHost = ReactiveControllerHost & - IgcComboComponent; +import { ComboRecord, ComboHost } from '../types.js'; const START_INDEX: Readonly = -1; diff --git a/src/components/combo/controllers/selection.ts b/src/components/combo/controllers/selection.ts new file mode 100644 index 000000000..b54bd1a91 --- /dev/null +++ b/src/components/combo/controllers/selection.ts @@ -0,0 +1,172 @@ +import { ReactiveController } from 'lit'; +import { ComboRecord, ComboHost, Values, ComboChangeType } from '../types.js'; + +export class SelectionController + implements ReactiveController +{ + private _selected: Set = new Set(); + + public getValue(items: T[]) { + return items + .map((value) => { + if (typeof value === 'object') { + return this.host.displayKey ? value[this.host.displayKey] : value; + } else { + return value; + } + }) + .join(', '); + } + + private handleChange(newValue: string, items: T[], type: ComboChangeType) { + return this.host.emitEvent('igcChange', { + cancelable: true, + detail: { newValue, items, type }, + }); + } + + private getItemsByValueKey(keys: Values[]) { + return keys.map((key) => + this.host.dataState.find((i) => i[this.host.valueKey!] === key) + ); + } + + private selectValueKeys(values: Values[]) { + if (values.length === 0) return; + + values.forEach((value) => { + const item = this.host.dataState.find( + (i) => i[this.host.valueKey!] === value + ); + + if (item) { + this._selected.add(item); + } + }); + } + + private deselectValueKeys(values: Values[]) { + if (values.length === 0) return; + + values.forEach((value) => { + const item = this.host.dataState.find( + (i) => i[this.host.valueKey!] === value + ); + + if (item) { + this._selected.delete(item); + } + }); + } + + private selectObjects(items: T[]) { + if (items.length === 0) return; + + items.forEach((item) => { + this._selected.add(item as ComboRecord); + }); + } + + private deselectObjects(items: T[]) { + if (items.length === 0) return; + + items.forEach((item) => { + this._selected.delete(item as ComboRecord); + }); + } + + private selectAll() { + this.host.dataState + .filter((i) => !i.header) + .forEach((item) => { + this._selected.add(item); + }); + + this.host.requestUpdate('selected'); + } + + private deselectAll() { + this._selected.clear(); + this.host.requestUpdate('selected'); + } + + public async select(items?: T[] | Values[], emit = false) { + if (!items || items.length === 0) { + this.selectAll(); + return; + } + + const values = this.getItemsByValueKey(items as Values[]); + const selected = Array.from(this._selected.values()); + const payload = [...values, ...selected] as T[]; + + if ( + emit && + !this.handleChange(this.getValue(payload), values as T[], 'selection') + ) { + return; + } + + if (this.host.valueKey) { + this.selectValueKeys(items as Values[]); + } else { + this.selectObjects(items as T[]); + } + + this.host.requestUpdate('selected'); + } + + public async deselect(items?: T[] | Values[], emit = false) { + if (!items || items.length === 0) { + this.deselectAll(); + return; + } + + const values = this.getItemsByValueKey(items as Values[]); + const selected = Array.from(this._selected.values()); + const payload = structuredClone(selected); + + payload.splice(selected.indexOf(values[0] as T)); + + if ( + emit && + !this.handleChange(this.getValue(payload), values as T[], 'deselection') + ) { + return; + } + + if (this.host.valueKey) { + this.deselectValueKeys(items as Values[]); + } else { + this.deselectObjects(items as T[]); + } + + this.host.requestUpdate('selected'); + } + + public get selected(): Set { + return this._selected; + } + + public changeSelection(index: number) { + const item = this.host.dataState[index]; + + if (this.host.valueKey) { + !this.selected.has(item) + ? this.select([item[this.host.valueKey]], true) + : this.deselect([item[this.host.valueKey]], true); + } else { + !this.selected.has(item) + ? this.select([item], true) + : this.deselect([item], true); + } + } + + constructor(protected host: ComboHost) { + this.host.addController(this); + } + + public hostConnected() {} + + public hostDisconnected() {} +} diff --git a/src/components/combo/types.ts b/src/components/combo/types.ts index 75070ed7f..cfb6d78d6 100644 --- a/src/components/combo/types.ts +++ b/src/components/combo/types.ts @@ -14,6 +14,7 @@ export type ComboHost = ReactiveControllerHost & IgcComboComponent; export type GroupingDirection = 'asc' | 'desc'; +export type ComboChangeType = 'selection' | 'deselection' | 'addition'; export interface FilteringOptions { filterKey: Keys | undefined; @@ -27,15 +28,19 @@ export interface GroupingOptions { direction: GroupingDirection; } +export interface IgcComboChangeEventArgs { + newValue: string; + items: object; + type: ComboChangeType; +} + export interface IgcComboEventMap { /* blazorSuppress */ - igcChange: CustomEvent; + igcChange: CustomEvent; igcFocus: CustomEvent; igcBlur: CustomEvent; - igcOpening: CustomEvent; - igcOpened: CustomEvent; - igcClosing: CustomEvent; - igcClosed: CustomEvent; - igcFiltering: CustomEvent; - igcFiltered: CustomEvent; + igcOpening: CustomEvent; + igcOpened: CustomEvent; + igcClosing: CustomEvent; + igcClosed: CustomEvent; } diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 064c9bb1e..8421162cb 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -12,11 +12,6 @@ const metadata = { title: 'Combo', component: 'igc-combo', argTypes: { - value: { - type: 'string | undefined', - description: 'The value attribute of the control.', - control: 'text', - }, name: { type: 'string', description: 'The name attribute of the control.', @@ -93,6 +88,10 @@ const metadata = { control: 'boolean', defaultValue: false, }, + value: { + type: 'string | undefined', + control: 'text', + }, scrollStrategy: { type: '"scroll" | "block" | "close" | undefined', options: ['scroll', 'block', 'close', 'undefined'], @@ -140,7 +139,6 @@ const metadata = { }; export default metadata; interface ArgTypes { - value: string | undefined; name: string; disabled: boolean; required: boolean; @@ -154,6 +152,7 @@ interface ArgTypes { dir: 'ltr' | 'rtl' | 'auto'; caseSensitiveIcon: boolean; disableFiltering: boolean; + value: string | undefined; scrollStrategy: 'scroll' | 'block' | 'close' | undefined; keepOpenOnOutsideClick: boolean | undefined; open: boolean; @@ -169,13 +168,11 @@ interface ArgTypes { handles: [ 'igcFocus', 'igcBlur', - 'igcChange', 'igcOpening', 'igcOpened', 'igcClosing', 'igcClosed', - 'igcFiltering', - 'igcFiltered', + 'igcChange', ], }, }; diff --git a/stories/stepper.stories.ts b/stories/stepper.stories.ts index 7f48f399d..ff51e2a4c 100644 --- a/stories/stepper.stories.ts +++ b/stories/stepper.stories.ts @@ -38,9 +38,9 @@ const metadata = { defaultValue: false, }, titlePosition: { - type: '"start" | "end" | "top" | "bottom" | undefined', + type: '"top" | "bottom" | "start" | "end" | undefined', description: 'Get/Set the position of the steps title.', - options: ['start', 'end', 'top', 'bottom', 'undefined'], + options: ['top', 'bottom', 'start', 'end', 'undefined'], control: { type: 'select', }, @@ -62,7 +62,7 @@ interface ArgTypes { stepType: 'indicator' | 'title' | 'full'; linear: boolean; contentTop: boolean; - titlePosition: 'start' | 'end' | 'top' | 'bottom' | undefined; + titlePosition: 'top' | 'bottom' | 'start' | 'end' | undefined; size: 'small' | 'medium' | 'large'; } // endregion From 41ceb59a2172823ddf1474411b1ef1fae328d640 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 30 Nov 2022 23:25:40 +0200 Subject: [PATCH 35/82] refactor(combo): partial ToggleComponent --- src/components/combo/combo.ts | 2 +- stories/combo.stories.ts | 57 +++++------------------------------ 2 files changed, 8 insertions(+), 51 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 885463f58..fbb7883b9 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -77,7 +77,7 @@ export default class IgcComboComponent extends EventEmitterMixin>( LitElement ) - implements IgcToggleComponent + implements Partial { public static readonly tagName = 'igc-combo'; public static styles = styles; diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 8421162cb..102da4d49 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -78,6 +78,12 @@ const metadata = { }, defaultValue: 'auto', }, + open: { + type: 'boolean', + description: 'Sets the open state of the component.', + control: 'boolean', + defaultValue: false, + }, caseSensitiveIcon: { type: 'boolean', control: 'boolean', @@ -92,49 +98,6 @@ const metadata = { type: 'string | undefined', control: 'text', }, - scrollStrategy: { - type: '"scroll" | "block" | "close" | undefined', - options: ['scroll', 'block', 'close', 'undefined'], - control: { - type: 'inline-radio', - }, - }, - keepOpenOnOutsideClick: { - type: 'boolean | undefined', - control: 'boolean | undefined', - }, - open: { - type: 'boolean', - description: 'Sets the open state of the component.', - control: 'boolean', - defaultValue: false, - }, - positionStrategy: { - type: '"absolute" | "fixed" | undefined', - description: - 'The positioning strategy to use.\nUse the `fixed` strategy when the target element is in a fixed container, otherwise - use `absolute`.', - options: ['absolute', 'fixed', 'undefined'], - control: { - type: 'inline-radio', - }, - }, - flip: { - type: 'boolean | undefined', - description: - "Whether the element should be flipped to the opposite side once it's about to overflow the visible area.\nOnce enough space is detected on its preferred side, it will flip back.", - control: 'boolean | undefined', - }, - distance: { - type: 'number | undefined', - description: - 'Whether to prevent the element from being cut off by moving it so it stays visible within its boundary area.', - control: 'number', - }, - sameWidth: { - type: 'boolean | undefined', - description: 'Whether to make the toggle the same width as the target.', - control: 'boolean | undefined', - }, }, }; export default metadata; @@ -150,16 +113,10 @@ interface ArgTypes { placeholder: string; placeholderSearch: string; dir: 'ltr' | 'rtl' | 'auto'; + open: boolean; caseSensitiveIcon: boolean; disableFiltering: boolean; value: string | undefined; - scrollStrategy: 'scroll' | 'block' | 'close' | undefined; - keepOpenOnOutsideClick: boolean | undefined; - open: boolean; - positionStrategy: 'absolute' | 'fixed' | undefined; - flip: boolean | undefined; - distance: number | undefined; - sameWidth: boolean | undefined; } // endregion From 41fecf2b1a2733bff4fb6d1893fb32ec89ae5e27 Mon Sep 17 00:00:00 2001 From: Marin Popov Date: Wed, 30 Nov 2022 23:50:58 +0200 Subject: [PATCH 36/82] input prefix/suffix (#568) * refactor(input): suffix/prefix Co-authored-by: Simeon Simeonoff --- .../input/themes/light/input.base.scss | 1 - .../input/themes/light/input.bootstrap.scss | 18 ++++- .../input/themes/light/input.fluent.scss | 16 ++++- .../input/themes/light/input.indigo.scss | 62 +++++++++------- .../input/themes/light/input.material.scss | 72 +++++++++++++------ 5 files changed, 122 insertions(+), 47 deletions(-) diff --git a/src/components/input/themes/light/input.base.scss b/src/components/input/themes/light/input.base.scss index 27f5bf0fc..55b671737 100644 --- a/src/components/input/themes/light/input.base.scss +++ b/src/components/input/themes/light/input.base.scss @@ -4,7 +4,6 @@ %starfix { display: flex; align-items: center; - height: 100%; color: color(gray, 700); } diff --git a/src/components/input/themes/light/input.bootstrap.scss b/src/components/input/themes/light/input.bootstrap.scss index 5c8357b31..bff800e30 100644 --- a/src/components/input/themes/light/input.bootstrap.scss +++ b/src/components/input/themes/light/input.bootstrap.scss @@ -174,10 +174,26 @@ $input-background: var(--input-background, #fff) !default; [part~='container'] { background: color(gray, 100); color: color(gray, 400); + border-block-color: color(gray, 300); + } + + [part='prefix'] { + border-inline-start-color: color(gray, 300); + } + + [part='suffix'] { + border-inline-end-color: color(gray, 300); } [part~='input'] { - border-color: color(gray, 300); + border-block-color: color(gray, 300); + border-inline-color: transparent; + } +} + +:host([disabled]) { + [part='helper-text'] { + color: color(gray, 400); } } diff --git a/src/components/input/themes/light/input.fluent.scss b/src/components/input/themes/light/input.fluent.scss index 8b9b0000d..dffba586c 100644 --- a/src/components/input/themes/light/input.fluent.scss +++ b/src/components/input/themes/light/input.fluent.scss @@ -23,12 +23,21 @@ $focused-height: calc(var(--size) - #{($focused-border-width * 2)}); [part='prefix'], [part='suffix'] { - padding: 0 rem(10px); color: color(gray, 700); background: color(gray, 100); font-size: rem(14px); } + [name='prefix']::slotted(*), + [name='suffix']::slotted(*) { + display: inline-flex; + align-items: center; + width: fit-content; + height: 100%; + padding-inline: pad-inline(8px, 12px, 16px); + padding-block: pad-block(7px, 9px, 11px); + } + [part='prefix'] { grid-area: 1 / 1; } @@ -101,6 +110,11 @@ $focused-height: calc(var(--size) - #{($focused-border-width * 2)}); [part='suffix'] { margin-inline-end: rem(-1px); } + + [name='prefix']::slotted(*), + [name='suffix']::slotted(*) { + padding-block: pad-block(6px, 8px, 10px); + } } :host(:not([invalid]):focus-within), diff --git a/src/components/input/themes/light/input.indigo.scss b/src/components/input/themes/light/input.indigo.scss index ba79101c0..fe0564719 100644 --- a/src/components/input/themes/light/input.indigo.scss +++ b/src/components/input/themes/light/input.indigo.scss @@ -1,34 +1,48 @@ -@use '../../../../styles/utilities' as utils; - -$label-focus-color: var(--label-focus-color, utils.color(primary, 500)) !default; +@use '../../../../styles/utilities' as *; + +$label-focus-color: var(--label-focus-color, color(primary, 500)) !default; + +%suffix-preffix { + display: inline-flex; + align-items: center; + width: fit-content; + box-sizing: border-box; + height: calc(100% - #{rem(2px)}); + padding-inline: pad-inline(2px, 4px, 8px); + padding-block: pad-block(9px, 11px, 13px); +} :host { [part='prefix'], [part='suffix'] { - padding-top: utils.rem(8px); - padding-bottom: utils.rem(8px); - color: utils.color(gray, 600); + color: color(gray, 600); } [part='prefix'] { - padding-inline-start: utils.rem(8px); grid-area: 1 / 1; + + ::slotted(*) { + @extend %suffix-preffix; + } } [part='suffix'] { - padding-inline-end: utils.rem(8px); grid-area: 1 / 3; + + ::slotted(*) { + @extend %suffix-preffix; + } } [part='label'] { - color: utils.color(gray, 600); - margin-bottom: utils.rem(4px); + color: color(gray, 600); + margin-bottom: rem(4px); font-size: initial; } [part^='container'] { background: transparent; - border-bottom: 1px solid utils.color(gray, 500); + border-bottom: 1px solid color(gray, 500); transition: background .25s ease-out; grid-template-columns: auto 1fr auto; @@ -36,11 +50,11 @@ $label-focus-color: var(--label-focus-color, utils.color(primary, 500)) !default position: absolute; content: ''; width: 100%; - height: utils.rem(2px); + height: rem(2px); left: 0; right: 0; bottom: -1px; - background: utils.color(secondary, 500); + background: color(secondary, 500); transform: scaleY(0); transition: transform 180ms cubic-bezier(.4, 0, .2, 1), opacity 180ms cubic-bezier(.4, 0, .2, 1); transform-origin: bottom center; @@ -48,30 +62,30 @@ $label-focus-color: var(--label-focus-color, utils.color(primary, 500)) !default } [part~='input'] { - font-size: utils.rem(16px); + font-size: rem(16px); background: initial; - padding: utils.rem(8px); + padding: rem(8px); border: none; grid-area: 1 / 2; } [part='helper-text'] { padding: 0; - color: utils.color(gray, 600); + color: color(gray, 600); } } :host(:hover), :host([outlined]:hover) { [part^='container'] { - background: utils.color(gray, 100); + background: color(gray, 100); } } :host(:focus-within), :host([outlined]:focus-within) { [part^='container'] { - background: utils.color(gray, 100); + background: color(gray, 100); &::after { transform: scaleY(1); @@ -85,16 +99,16 @@ $label-focus-color: var(--label-focus-color, utils.color(primary, 500)) !default :host([invalid]) { [part^='container'] { - border-color: utils.color(error, 500); + border-color: color(error, 500); &::after { - background: utils.color(error, 500); + background: color(error, 500); } } [part='label'], [part^='helper-text'] { - color: utils.color(error, 500); + color: color(error, 500); } } @@ -102,14 +116,14 @@ $label-focus-color: var(--label-focus-color, utils.color(primary, 500)) !default pointer-events: none; [part^='container'] { - border-color: utils.color(gray, 300); - color: utils.color(gray, 300); + border-color: color(gray, 300); + color: color(gray, 300); } [part='prefix'], [part='suffix'], [part='label'], [part='helper-text'] { - color: utils.color(gray, 300); + color: color(gray, 300); } } diff --git a/src/components/input/themes/light/input.material.scss b/src/components/input/themes/light/input.material.scss index 2db27940c..85fb38650 100644 --- a/src/components/input/themes/light/input.material.scss +++ b/src/components/input/themes/light/input.material.scss @@ -30,6 +30,16 @@ $fs: rem(16px) !default; border-top: $idle-border-width solid transparent; } +%suffix-preffix { + display: inline-flex; + align-items: center; + width: fit-content; + box-sizing: border-box; + height: 100%; + padding-inline: pad-inline(8px); + padding-block: pad-block(11px, 13px, 15px); +} + :host { input:placeholder-shown + [part='notch'], [part~='filled'] + [part='notch'] { @@ -137,14 +147,6 @@ $fs: rem(16px) !default; } } - [part='prefix'] { - padding-inline-start: rem(4px); - } - - [part='suffix'] { - padding-inline-end: rem(4px); - } - [part='label'] { color: $active-color; } @@ -155,9 +157,7 @@ $fs: rem(16px) !default; } [part='start'] { - width: auto; - min-width: rem(12px); - flex-shrink: 0; + justify-content: flex-start; grid-area: 1 / 1; border: { style: solid; @@ -179,8 +179,9 @@ $fs: rem(16px) !default; } > [part='prefix'] { - padding-inline-start: rem(5px); - padding-inline-end: rem(4px); + ::slotted(*) { + @extend %suffix-preffix; + } } } @@ -218,12 +219,8 @@ $fs: rem(16px) !default; } [part='end'] { - display: flex; justify-content: flex-end; - flex-grow: 1; - height: 100%; grid-area: 1 / 4; - min-width: rem(12px); border: { style: solid; color: $idle-color; @@ -244,11 +241,20 @@ $fs: rem(16px) !default; } > [part='suffix'] { - padding-inline-start: rem(4px); - padding-inline-end: rem(5px); + ::slotted(*) { + @extend %suffix-preffix; + } } } +[part='start'], +[part='end'] { + display: flex; + min-width: rem(12px); + height: var(--size); + overflow: hidden; +} + [part='helper-text'] { @include type-style('caption'); @@ -311,11 +317,37 @@ $fs: rem(16px) !default; } // Filled Style +:host([outlined]:focus), +:host([outlined]:focus-within) { + [part='suffix'] { + margin-inline-end: rem(-1px); + } + + [part='prefix'] { + margin-inline-start: rem(-1px); + } + + [part='suffix'], + [part='prefix'] { + ::slotted(*) { + padding-block: pad-block(10px, 12px, 14px); + } + } +} + :host(:not([outlined])) { [part='start'], [part='end'] { border-color: transparent; - border-top-width: rem(1px); + border-width: rem(1px); + } + + [part='start'] { + border-end-start-radius: 0; + } + + [part='end'] { + border-end-end-radius: 0; } [part='notch'], From a86b1a711796470d2ff783d71feb2b698eb47cd0 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 1 Dec 2022 10:26:55 +0200 Subject: [PATCH 37/82] refactor(combo): fix selection controller --- src/components/combo/combo.ts | 15 +--- src/components/combo/controllers/selection.ts | 74 +++++++++---------- stories/combo.stories.ts | 4 +- 3 files changed, 43 insertions(+), 50 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index fbb7883b9..31d1c5a42 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -82,8 +82,6 @@ export default class IgcComboComponent public static readonly tagName = 'igc-combo'; public static styles = styles; - private _value?: string | undefined; - protected navigationController = new NavigationController(this); protected selectionController = new SelectionController(this); protected dataController = new DataController(this); @@ -217,13 +215,6 @@ export default class IgcComboComponent this.navigationController.active = 0; } - @watch('selected', { waitUntilFirstUpdate: true }) - protected updateValue() { - const { selected } = this.selectionController; - const values = Array.from(selected.values()); - this._value = this.selectionController.getValue(values); - } - @property({ attribute: false }) public itemTemplate: (item: ComboRecord) => TemplateResult = (item) => { if (this.displayKey) { @@ -263,7 +254,9 @@ export default class IgcComboComponent } public get value() { - return this._value; + return this.selectionController.getValue( + Array.from(this.selectionController.selected) + ); } public override async firstUpdated() { @@ -404,7 +397,7 @@ export default class IgcComboComponent part="target" exportparts="container: input, input: native-input, label, prefix, suffix" @click=${this.toggle} - value=${ifDefined(this._value)} + value=${ifDefined(this.value)} placeholder=${ifDefined(this.placeholder)} label=${ifDefined(this.label)} dir=${this.dir} diff --git a/src/components/combo/controllers/selection.ts b/src/components/combo/controllers/selection.ts index b54bd1a91..c886316d7 100644 --- a/src/components/combo/controllers/selection.ts +++ b/src/components/combo/controllers/selection.ts @@ -1,5 +1,5 @@ import { ReactiveController } from 'lit'; -import { ComboRecord, ComboHost, Values, ComboChangeType } from '../types.js'; +import { ComboHost, Values, IgcComboChangeEventArgs } from '../types.js'; export class SelectionController implements ReactiveController @@ -18,11 +18,8 @@ export class SelectionController .join(', '); } - private handleChange(newValue: string, items: T[], type: ComboChangeType) { - return this.host.emitEvent('igcChange', { - cancelable: true, - detail: { newValue, items, type }, - }); + private handleChange(detail: IgcComboChangeEventArgs) { + return this.host.emitEvent('igcChange', { cancelable: true, detail }); } private getItemsByValueKey(keys: Values[]) { @@ -31,31 +28,19 @@ export class SelectionController ); } - private selectValueKeys(values: Values[]) { - if (values.length === 0) return; - - values.forEach((value) => { - const item = this.host.dataState.find( - (i) => i[this.host.valueKey!] === value - ); + private selectValueKeys(keys: Values[]) { + if (keys.length === 0) return; - if (item) { - this._selected.add(item); - } + this.getItemsByValueKey(keys).forEach((item) => { + return item && this._selected.add(item); }); } - private deselectValueKeys(values: Values[]) { - if (values.length === 0) return; + private deselectValueKeys(keys: Values[]) { + if (keys.length === 0) return; - values.forEach((value) => { - const item = this.host.dataState.find( - (i) => i[this.host.valueKey!] === value - ); - - if (item) { - this._selected.delete(item); - } + this.getItemsByValueKey(keys).forEach((item) => { + return item && this._selected.delete(item); }); } @@ -63,7 +48,9 @@ export class SelectionController if (items.length === 0) return; items.forEach((item) => { - this._selected.add(item as ComboRecord); + if (this.host.dataState.find((i) => !i.header && i === item)) { + this._selected.add(item); + } }); } @@ -71,7 +58,9 @@ export class SelectionController if (items.length === 0) return; items.forEach((item) => { - this._selected.delete(item as ComboRecord); + if (this.host.dataState.find((i) => !i.header && i === item)) { + this._selected.delete(item); + } }); } @@ -81,13 +70,12 @@ export class SelectionController .forEach((item) => { this._selected.add(item); }); - - this.host.requestUpdate('selected'); + this.host.requestUpdate(); } private deselectAll() { this._selected.clear(); - this.host.requestUpdate('selected'); + this.host.requestUpdate(); } public async select(items?: T[] | Values[], emit = false) { @@ -96,13 +84,19 @@ export class SelectionController return; } - const values = this.getItemsByValueKey(items as Values[]); + const values = this.host.valueKey + ? this.getItemsByValueKey(items as Values[]) + : items; const selected = Array.from(this._selected.values()); const payload = [...values, ...selected] as T[]; if ( emit && - !this.handleChange(this.getValue(payload), values as T[], 'selection') + !this.handleChange({ + newValue: this.getValue(payload), + items: values as T[], + type: 'selection', + }) ) { return; } @@ -113,7 +107,7 @@ export class SelectionController this.selectObjects(items as T[]); } - this.host.requestUpdate('selected'); + this.host.requestUpdate(); } public async deselect(items?: T[] | Values[], emit = false) { @@ -122,7 +116,9 @@ export class SelectionController return; } - const values = this.getItemsByValueKey(items as Values[]); + const values = this.host.valueKey + ? this.getItemsByValueKey(items as Values[]) + : items; const selected = Array.from(this._selected.values()); const payload = structuredClone(selected); @@ -130,7 +126,11 @@ export class SelectionController if ( emit && - !this.handleChange(this.getValue(payload), values as T[], 'deselection') + !this.handleChange({ + newValue: this.getValue(payload), + items: values as T[], + type: 'deselection', + }) ) { return; } @@ -141,7 +141,7 @@ export class SelectionController this.deselectObjects(items as T[]); } - this.host.requestUpdate('selected'); + this.host.requestUpdate(); } public get selected(): Set { diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 102da4d49..92c3bbaaa 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -95,7 +95,7 @@ const metadata = { defaultValue: false, }, value: { - type: 'string | undefined', + type: 'string', control: 'text', }, }, @@ -116,7 +116,7 @@ interface ArgTypes { open: boolean; caseSensitiveIcon: boolean; disableFiltering: boolean; - value: string | undefined; + value: string; } // endregion From 48ac5b075b75b49be5f5be3c30aa6a283a8f3c73 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 1 Dec 2022 13:02:39 +0200 Subject: [PATCH 38/82] fix(combo): bug fixes --- package-lock.json | 17 ----- package.json | 1 - src/components/combo/combo.ts | 11 ++-- src/components/combo/controllers/selection.ts | 19 ++++-- src/components/combo/operations/filter.ts | 4 +- .../combo/themes/light/combo.base.scss | 7 +- stories/combo.stories.ts | 65 +++++-------------- 7 files changed, 43 insertions(+), 81 deletions(-) diff --git a/package-lock.json b/package-lock.json index f044cb390..8b39b37f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "lit": "^2.4.0" }, "devDependencies": { - "@faker-js/faker": "^7.6.0", "@igniteui/material-icons-extended": "^2.11.0", "@lit-labs/virtualizer": "^0.7.2", "@open-wc/eslint-config": "^8.0.2", @@ -1782,16 +1781,6 @@ "@types/chai": "^4.2.12" } }, - "node_modules/@faker-js/faker": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", - "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", - "dev": true, - "engines": { - "node": ">=14.0.0", - "npm": ">=6.0.0" - } - }, "node_modules/@floating-ui/core": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.1.tgz", @@ -18260,12 +18249,6 @@ "@types/chai": "^4.2.12" } }, - "@faker-js/faker": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", - "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", - "dev": true - }, "@floating-ui/core": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.1.tgz", diff --git a/package.json b/package.json index 5c11f8943..05052fa4c 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "lit": "^2.4.0" }, "devDependencies": { - "@faker-js/faker": "^7.6.0", "@igniteui/material-icons-extended": "^2.11.0", "@lit-labs/virtualizer": "^0.7.2", "@open-wc/eslint-config": "^8.0.2", diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 31d1c5a42..fba3e667e 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -221,14 +221,14 @@ export default class IgcComboComponent return html`${item[this.displayKey]}`; } - return html`${item}`; + return html`${String(item)}`; }; @property({ attribute: false }) public headerItemTemplate: (item: ComboRecord) => TemplateResult = ( item: ComboRecord ) => { - return html`${item[this.groupKey!]}`; + return html`${this.groupKey && item[this.groupKey]}`; }; constructor() { @@ -328,11 +328,12 @@ export default class IgcComboComponent const record = item as ComboRecord; const { selected } = this.selectionController; - const headerTemplate = html`${this.headerItemTemplate(record)}`; const itemTemplate = html` >${this.itemTemplate(record)}`; - return html`${record.header ? headerTemplate : itemTemplate}`; + return html`${this.groupKey && record.header + ? headerTemplate + : itemTemplate}`; }; protected keydownHandler(event: KeyboardEvent) { diff --git a/src/components/combo/controllers/selection.ts b/src/components/combo/controllers/selection.ts index c886316d7..0e5986c6e 100644 --- a/src/components/combo/controllers/selection.ts +++ b/src/components/combo/controllers/selection.ts @@ -1,5 +1,10 @@ import { ReactiveController } from 'lit'; -import { ComboHost, Values, IgcComboChangeEventArgs } from '../types.js'; +import { + ComboRecord, + ComboHost, + Values, + IgcComboChangeEventArgs, +} from '../types.js'; export class SelectionController implements ReactiveController @@ -10,9 +15,11 @@ export class SelectionController return items .map((value) => { if (typeof value === 'object') { - return this.host.displayKey ? value[this.host.displayKey] : value; + return this.host.displayKey + ? String(value[this.host.displayKey]) + : value; } else { - return value; + return String(value); } }) .join(', '); @@ -48,7 +55,8 @@ export class SelectionController if (items.length === 0) return; items.forEach((item) => { - if (this.host.dataState.find((i) => !i.header && i === item)) { + const i = this.host.dataState.includes(item as ComboRecord); + if (i) { this._selected.add(item); } }); @@ -58,7 +66,8 @@ export class SelectionController if (items.length === 0) return; items.forEach((item) => { - if (this.host.dataState.find((i) => !i.header && i === item)) { + const i = this.host.dataState.includes(item as ComboRecord); + if (i) { this._selected.delete(item); } }); diff --git a/src/components/combo/operations/filter.ts b/src/components/combo/operations/filter.ts index 220c4cb2e..cfeee8af3 100644 --- a/src/components/combo/operations/filter.ts +++ b/src/components/combo/operations/filter.ts @@ -12,9 +12,7 @@ export default class FilterDataOperation { const term = caseSensitive ? searchTerm : searchTerm.toLocaleLowerCase(); return data.filter((item: T) => { - const value = filterKey - ? (item[filterKey] as any).toString() - : item.toString(); + const value = filterKey ? String(item[filterKey] as any) : String(item); return caseSensitive ? value.includes(term) diff --git a/src/components/combo/themes/light/combo.base.scss b/src/components/combo/themes/light/combo.base.scss index c4be96af6..4793d410a 100644 --- a/src/components/combo/themes/light/combo.base.scss +++ b/src/components/combo/themes/light/combo.base.scss @@ -2,6 +2,7 @@ @use '../../../../styles/utilities' as *; :host { + display: block; font-family: var(--ig-font-family); } @@ -17,10 +18,6 @@ igc-input::part(input) { } } -// igc-input::part(container) { -// --input-background: transparent; -// } - :host([disabled]) { ::slotted([slot='helper-text']) { color: color(gray, 400); @@ -36,6 +33,8 @@ igc-input::part(input) { [part='list-wrapper'] { @include border-radius(rem(4px)); + position: absolute; + width: 100%; background: color(surface); color: color(gray, 700); box-shadow: elevation(8); diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 92c3bbaaa..6ef365e2b 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -3,7 +3,6 @@ import { Context, Story } from './story.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { defineAllComponents } from '../src/index.js'; import { registerIconFromText } from '../src/components/icon/icon.registry'; -// import { faker } from '@faker-js/faker'; defineAllComponents(); @@ -134,41 +133,17 @@ interface ArgTypes { }, }; -// interface City { -// id: string; -// name: string; -// zip: string; -// country: string; -// } - -// function generateCity(): City { -// const id = faker.datatype.uuid(); -// const name = faker.address.cityName(); -// const zip = faker.address.zipCode(); -// const country = faker.address.country(); - -// return { -// id, -// name, -// zip, -// country, -// }; -// } - -// function generateCities(amount = 200) { -// const result: Array = []; - -// for (let i = 0; i <= amount; i++) { -// result.push(generateCity()); -// } - -// return result; -// } +interface City { + id: string; + name: string; + zip: string; + country: string; +} // const itemTemplate = (item: City) => { // return html` //
-// ${item.name}, ${item.country} +// ${item.name} [${item.zip}] //
// `; // }; @@ -177,67 +152,64 @@ interface ArgTypes { // return html`Group header for ${item.country}`; // }; -// .itemTemplate=${itemTemplate} -// .headerItemTemplate=${headerItemTemplate} -// const cities = generateCities(200); -const cities = [ +const cities: City[] = [ { id: 'BG01', name: 'Sofia', country: 'Bulgaria', - zip: 1000, + zip: '1000', }, { id: 'BG02', name: 'Plovdiv', country: 'Bulgaria', - zip: 4000, + zip: '4000', }, { id: 'BG03', name: 'Varna', country: 'Bulgaria', - zip: 9000, + zip: '9000', }, { id: 'US01', name: 'New York', country: 'United States', - zip: 1000, + zip: '1000', }, { id: 'US02', name: 'Boston', country: 'United States', - zip: 4000, + zip: '4000', }, { id: 'US03', name: 'San Francisco', country: 'United States', - zip: 9000, + zip: '9000', }, { id: 'JP01', name: 'Tokyo', country: 'Japan', - zip: 1000, + zip: '1000', }, { id: 'JP02', name: 'Yokohama', country: 'Japan', - zip: 4000, + zip: '4000', }, { id: 'JP03', name: 'Osaka', country: 'Japan', - zip: 9000, + zip: '9000', }, ]; -// const simpleCities = ['Sofia', 4, 'Varna', 'varna', false, { a: 1, b: 2 }, -1, true]; +// const mandzhasgrozde = [0, 'Sofia', 4, 'Varna', 'varna', false, { a: 1, b: 2 }, -1, true, null, undefined, NaN, 0]; registerIconFromText( 'location', @@ -270,7 +242,6 @@ const Template: Story = ( placeholder=${ifDefined(placeholder)} placeholder-search=${ifDefined(placeholderSearch)} dir=${ifDefined(direction)} - value-key="id" display-key="name" group-key="country" group-sorting="asc" From 4ef4f35c6d082cb55278ceb47049e5ca0b574d2a Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 1 Dec 2022 13:16:44 +0200 Subject: [PATCH 39/82] refactor(combo): fix item click handler --- src/components/combo/combo.ts | 7 ++++++- stories/combo.stories.ts | 35 +++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index fba3e667e..b270d087a 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -359,7 +359,12 @@ export default class IgcComboComponent } protected itemClickHandler(event: MouseEvent) { - const target = event.target as IgcComboItemComponent; + const target = event + .composedPath() + .find( + (el) => el instanceof IgcComboItemComponent + ) as IgcComboItemComponent; + this.toggleSelect(target.index); this.input.focus(); } diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 6ef365e2b..bcf167cd2 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -140,17 +140,17 @@ interface City { country: string; } -// const itemTemplate = (item: City) => { -// return html` -//
-// ${item.name} [${item.zip}] -//
-// `; -// }; +const itemTemplate = (item: City) => { + return html` +
+ ${item.name} [${item.zip}] +
+ `; +}; -// const headerItemTemplate = (item: City) => { -// return html`Group header for ${item.country}`; -// }; +const headerItemTemplate = (item: City) => { + return html`Country of ${item.country}`; +}; const cities: City[] = [ { @@ -175,37 +175,37 @@ const cities: City[] = [ id: 'US01', name: 'New York', country: 'United States', - zip: '1000', + zip: '10001', }, { id: 'US02', name: 'Boston', country: 'United States', - zip: '4000', + zip: '02108', }, { id: 'US03', name: 'San Francisco', country: 'United States', - zip: '9000', + zip: '94103', }, { id: 'JP01', name: 'Tokyo', country: 'Japan', - zip: '1000', + zip: '163-8001', }, { id: 'JP02', name: 'Yokohama', country: 'Japan', - zip: '4000', + zip: '781-0240', }, { id: 'JP03', name: 'Osaka', country: 'Japan', - zip: '9000', + zip: '552-0021', }, ]; @@ -237,11 +237,14 @@ const Template: Story = ( Date: Thu, 1 Dec 2022 13:56:04 +0200 Subject: [PATCH 40/82] refactor(combo): add item parts --- src/components/combo/combo.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index b270d087a..939d42620 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -332,8 +332,14 @@ export default class IgcComboComponent >${this.headerItemTemplate(record)}`; + const itemParts = partNameMap({ + item: true, + selected: selected.has(item), + active: this.navigationController.active === index, + }); + const itemTemplate = html` Date: Thu, 1 Dec 2022 15:24:03 +0200 Subject: [PATCH 41/82] fix(combo): more bug fixes --- src/components/combo/combo.ts | 14 +++++++++----- src/components/combo/controllers/selection.ts | 4 ++-- src/components/combo/operations/group.ts | 4 +++- stories/combo.stories.ts | 8 ++++---- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 939d42620..f098a5b8b 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -217,6 +217,10 @@ export default class IgcComboComponent @property({ attribute: false }) public itemTemplate: (item: ComboRecord) => TemplateResult = (item) => { + if (typeof item !== 'object' || item === null) { + return String(item) as any; + } + if (this.displayKey) { return html`${item[this.displayKey]}`; } @@ -225,7 +229,7 @@ export default class IgcComboComponent }; @property({ attribute: false }) - public headerItemTemplate: (item: ComboRecord) => TemplateResult = ( + public groupHeaderTemplate: (item: ComboRecord) => TemplateResult = ( item: ComboRecord ) => { return html`${this.groupKey && item[this.groupKey]}`; @@ -320,8 +324,8 @@ export default class IgcComboComponent emit && this.emitEvent('igcClosed'); } - public toggle() { - this.open ? this.hide(true) : this.show(true); + public toggle(emit = false) { + this.open ? this.hide(emit) : this.show(emit); } protected itemRenderer = (item: T, index: number): TemplateResult => { @@ -329,7 +333,7 @@ export default class IgcComboComponent const { selected } = this.selectionController; const headerTemplate = html`${this.headerItemTemplate(record)}${this.groupHeaderTemplate(record)}`; const itemParts = partNameMap({ @@ -410,7 +414,7 @@ export default class IgcComboComponent this.toggle(true)} value=${ifDefined(this.value)} placeholder=${ifDefined(this.placeholder)} label=${ifDefined(this.label)} diff --git a/src/components/combo/controllers/selection.ts b/src/components/combo/controllers/selection.ts index 0e5986c6e..fadf9df52 100644 --- a/src/components/combo/controllers/selection.ts +++ b/src/components/combo/controllers/selection.ts @@ -14,10 +14,10 @@ export class SelectionController public getValue(items: T[]) { return items .map((value) => { - if (typeof value === 'object') { + if (typeof value === 'object' && value !== null) { return this.host.displayKey ? String(value[this.host.displayKey]) - : value; + : String(value); } else { return String(value); } diff --git a/src/components/combo/operations/group.ts b/src/components/combo/operations/group.ts index 0212478a2..b3dec5702 100644 --- a/src/components/combo/operations/group.ts +++ b/src/components/combo/operations/group.ts @@ -44,7 +44,9 @@ export default class GroupDataOperation { const result = new Map(); data.forEach((item: T) => { - const key = item[groupKey!]; + if (typeof item !== 'object' || item === null) return; + + const key = item[groupKey!] ?? 'Other'; const group = result.get(key) ?? >[]; if (group.length === 0) { diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index bcf167cd2..a5148c4bf 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -143,13 +143,13 @@ interface City { const itemTemplate = (item: City) => { return html`
- ${item.name} [${item.zip}] + ${item?.name ?? item} [${item?.zip}]
`; }; -const headerItemTemplate = (item: City) => { - return html`Country of ${item.country}`; +const groupHeaderTemplate = (item: City) => { + return html`
Country of ${item?.country ?? item}
`; }; const cities: City[] = [ @@ -238,7 +238,7 @@ const Template: Story = ( .data=${cities} .dir=${direction} .itemTemplate=${itemTemplate} - .headerItemTemplate=${headerItemTemplate} + .groupHeaderTemplate=${groupHeaderTemplate} label=${ifDefined(label)} name=${ifDefined(name)} placeholder=${ifDefined(placeholder)} From 16c1d664ff9a68ce27c3ea5d7f473b6500ffb352 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 1 Dec 2022 16:07:38 +0200 Subject: [PATCH 42/82] refactor(combo): expose parts for styling --- src/components/combo/combo-item.ts | 6 +++++- src/components/combo/combo.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/combo/combo-item.ts b/src/components/combo/combo-item.ts index e6d678ed4..5e1004fea 100644 --- a/src/components/combo/combo-item.ts +++ b/src/components/combo/combo-item.ts @@ -46,7 +46,11 @@ export default class IgcComboItemComponent extends LitElement { protected override render() { return html`
- +
diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index f098a5b8b..882f262dc 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -344,6 +344,7 @@ export default class IgcComboComponent const itemTemplate = html` Date: Thu, 1 Dec 2022 17:33:55 +0200 Subject: [PATCH 43/82] Combo search (#569) * theme(combo, select, input): various fixes and improvements Co-authored-by: Simeon Simeonoff --- src/components/combo/combo.ts | 1 + .../combo/themes/light/combo.base.scss | 45 ++++++++++++++----- .../combo/themes/light/combo.bootstrap.scss | 24 +++++++++- .../combo/themes/light/combo.fluent.scss | 28 +++++++++++- .../combo/themes/light/combo.indigo.scss | 16 ++++++- .../combo/themes/light/combo.material.scss | 39 +++++++++++++++- .../themes/light/item/combo-item.base.scss | 5 ++- .../input/themes/light/input.fluent.scss | 1 + .../input/themes/light/input.indigo.scss | 2 +- .../input/themes/light/input.material.scss | 3 +- .../select/themes/light/select.bootstrap.scss | 36 +++++++-------- .../select/themes/light/select.material.scss | 28 ++++-------- 12 files changed, 169 insertions(+), 59 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 882f262dc..42e8321e9 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -413,6 +413,7 @@ export default class IgcComboComponent return html` this.toggle(true)} diff --git a/src/components/combo/themes/light/combo.base.scss b/src/components/combo/themes/light/combo.base.scss index 4793d410a..7b8719e20 100644 --- a/src/components/combo/themes/light/combo.base.scss +++ b/src/components/combo/themes/light/combo.base.scss @@ -2,30 +2,34 @@ @use '../../../../styles/utilities' as *; :host { + --component-size: var(--ig-size, var(--ig-size-medium)); + display: block; font-family: var(--ig-font-family); -} -igc-input { - cursor: pointer; + > igc-input::part(input) { + cursor: pointer; + } } -igc-input::part(input) { - cursor: pointer; - - &::selection { - background: transparent; - } +igc-input::part(helper-text) { + position: absolute; } :host([disabled]) { + igc-input::part(label), + igc-input::part(input)::placeholder, ::slotted([slot='helper-text']) { color: color(gray, 400); } + + pointer-events: none; + user-select: none; } [part='clear-icon'], -[part='toggle-icon'] { +[part='toggle-icon'], +[part~='case-icon'] { display: flex; cursor: pointer; } @@ -65,11 +69,20 @@ igc-input::part(input) { border-inline-end: 0; border-block-start: 0; box-shadow: none; + + &::selection { + background: color(primary, 100); + } + } + + [part='case-icon active'] { + color: color(primary, 500); } } [part='filter-input'] { - padding: rem(8px) rem(16px); + padding-inline: pad-inline(4px, 8px, 16px); + padding-block: pad-block(8px); z-index: 26; } @@ -85,3 +98,13 @@ igc-input::part(input) { [part='case-icon active'] { color: color(gray, 900); } + +[part='empty'] { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + color: color(gray, 700); + padding: rem(16px) rem(24px); + font-size: rem(13px); +} diff --git a/src/components/combo/themes/light/combo.bootstrap.scss b/src/components/combo/themes/light/combo.bootstrap.scss index 816354d4b..d1efdc832 100644 --- a/src/components/combo/themes/light/combo.bootstrap.scss +++ b/src/components/combo/themes/light/combo.bootstrap.scss @@ -1,6 +1,28 @@ @use '../../../../styles/utilities' as *; -@use '../../../select/themes/light/select.bootstrap' as *; +@use '../../../select/themes/light/select.bootstrap'; [part='clear-icon'] { border-inline-end: 1px solid color(gray, 400); } + +[part='list-wrapper'] { + igc-input::part(suffix) { + background: transparent; + border: none; + border-radius: 0; + } + + [part='search-input'] { + [part~='case-icon'] { + border-block-end: 1px solid color(gray, 400); + transition: border .15s ease-out; + } + + &:focus, + &:focus-within { + [part~='case-icon'] { + border-block-end: 1px solid color(primary, 500); + } + } + } +} diff --git a/src/components/combo/themes/light/combo.fluent.scss b/src/components/combo/themes/light/combo.fluent.scss index 859d50372..079f2e468 100644 --- a/src/components/combo/themes/light/combo.fluent.scss +++ b/src/components/combo/themes/light/combo.fluent.scss @@ -1 +1,27 @@ -@use '../../../select/themes/light/select.fluent' as *; +@use '../../../../styles/utilities' as *; +@use '../../../select/themes/light/select.fluent'; + +:host { + ::slotted([slot='helper-text']) { + @include type-style('caption'); + + display: block; + padding: 0; + margin-top: rem(8px); + color: color(gray, 600); + } +} + +[part='filter-input'] { + padding: pad(2px, 4px, 8px); +} + +[part='list-wrapper'] { + igc-input::part(suffix) { + background: transparent; + } + + igc-input::part(input) { + height: var(--size); + } +} diff --git a/src/components/combo/themes/light/combo.indigo.scss b/src/components/combo/themes/light/combo.indigo.scss index 671065b89..9efab5a9e 100644 --- a/src/components/combo/themes/light/combo.indigo.scss +++ b/src/components/combo/themes/light/combo.indigo.scss @@ -1 +1,15 @@ -@use '../../../select/themes/light/select.indigo' as *; +@use '../../../../styles/utilities' as *; +@use '../../../select/themes/light/select.indigo'; + +[part~='case-icon'] { + padding: rem(8px); +} + +:host { + ::slotted([slot='helper-text']) { + @include type-style('caption'); + + padding: 0; + color: color(gray, 600); + } +} diff --git a/src/components/combo/themes/light/combo.material.scss b/src/components/combo/themes/light/combo.material.scss index fd4432949..f556f5011 100644 --- a/src/components/combo/themes/light/combo.material.scss +++ b/src/components/combo/themes/light/combo.material.scss @@ -1 +1,38 @@ -@use '../../../select/themes/light/select.material' as *; +@use '../../../../styles/utilities' as *; +@use '../../../select/themes/light/select.material'; + +[part='toggle-icon'] { + background: color(gray, 300); +} + +[part='target']:not([outlined]) { + &:focus-within { + [part='toggle-icon'] { + background: color(gray, 400, .3); + } + } +} + +:host([outlined]:focus-within) { + igc-input::part(suffix) { + margin-inline-end: -1px; + } + + igc-input::part(prefix) { + margin-inline-start: -1px; + } +} + +[part~='case-icon'] { + padding: rem(8px); +} + +:host { + ::slotted([slot='helper-text']) { + @include type-style('caption'); + + padding: 0 rem(8px); + margin-top: rem(4px); + color: color(gray, 600); + } +} diff --git a/src/components/combo/themes/light/item/combo-item.base.scss b/src/components/combo/themes/light/item/combo-item.base.scss index 20cf8e816..288139765 100644 --- a/src/components/combo/themes/light/item/combo-item.base.scss +++ b/src/components/combo/themes/light/item/combo-item.base.scss @@ -2,9 +2,10 @@ @use '../../../../dropdown/themes/light/dropdown-item.base' as *; :host { - gap: rem(8px); - --component-size: var(--ig-size, var(--ig-size-medium)); + + gap: rem(8px); + border-radius: 0 !important; } igc-checkbox { diff --git a/src/components/input/themes/light/input.fluent.scss b/src/components/input/themes/light/input.fluent.scss index dffba586c..c884e0562 100644 --- a/src/components/input/themes/light/input.fluent.scss +++ b/src/components/input/themes/light/input.fluent.scss @@ -26,6 +26,7 @@ $focused-height: calc(var(--size) - #{($focused-border-width * 2)}); color: color(gray, 700); background: color(gray, 100); font-size: rem(14px); + cursor: default; } [name='prefix']::slotted(*), diff --git a/src/components/input/themes/light/input.indigo.scss b/src/components/input/themes/light/input.indigo.scss index fe0564719..0d8a3af1f 100644 --- a/src/components/input/themes/light/input.indigo.scss +++ b/src/components/input/themes/light/input.indigo.scss @@ -8,7 +8,7 @@ $label-focus-color: var(--label-focus-color, color(primary, 500)) !default; width: fit-content; box-sizing: border-box; height: calc(100% - #{rem(2px)}); - padding-inline: pad-inline(2px, 4px, 8px); + padding-inline: pad-inline(8px, 12px, 16px); padding-block: pad-block(9px, 11px, 13px); } diff --git a/src/components/input/themes/light/input.material.scss b/src/components/input/themes/light/input.material.scss index 85fb38650..cb54c9e29 100644 --- a/src/components/input/themes/light/input.material.scss +++ b/src/components/input/themes/light/input.material.scss @@ -36,7 +36,7 @@ $fs: rem(16px) !default; width: fit-content; box-sizing: border-box; height: 100%; - padding-inline: pad-inline(8px); + padding-inline: pad-inline(8px, 12px, 16px); padding-block: pad-block(11px, 13px, 15px); } @@ -317,6 +317,7 @@ $fs: rem(16px) !default; } // Filled Style +:host([outlined]:active), :host([outlined]:focus), :host([outlined]:focus-within) { [part='suffix'] { diff --git a/src/components/select/themes/light/select.bootstrap.scss b/src/components/select/themes/light/select.bootstrap.scss index e04add4bc..5710db535 100644 --- a/src/components/select/themes/light/select.bootstrap.scss +++ b/src/components/select/themes/light/select.bootstrap.scss @@ -1,54 +1,50 @@ -@use '../../../../styles/utilities' as utils; +@use '../../../../styles/utilities' as *; $input-background: var(--input-background, #fff) !default; :host { ::slotted([slot='helper-text']) { - @include utils.type-style('body-2'); + @include type-style('body-2'); - color: utils.color(gray, 700); - } -} - -:host([disabled]) { - ::slotted([slot='helper-text']) { - color: utils.color(gray, 700); + color: color(gray, 700); } } :host(:focus-within) { igc-input[readonly]:not([disabled])::part(input) { - box-shadow: 0 0 0 utils.rem(4px) utils.color(primary, 500, .38); - border-color: utils.color(primary, 500); + box-shadow: 0 0 0 rem(4px) color(primary, 500, .38); + border-color: color(primary, 500); } } :host([invalid]), :host([invalid]:focus-within) { igc-input[readonly]:not([disabled])::part(input) { - box-shadow: 0 0 0 utils.rem(4px) utils.color(error, 500, .38); - border-color: utils.color(error, 500); + box-shadow: 0 0 0 rem(4px) color(error, 500, .38); + border-color: color(error, 500); } } igc-input[readonly]:not([disabled])::part(prefix), igc-input[readonly]:not([disabled])::part(suffix) { - color: utils.color(gray, 800); - background: utils.color(gray, 300); + color: color(gray, 800); + background: color(gray, 300); } igc-input[readonly]:not([disabled])::part(prefix), igc-input[readonly]:not([disabled])::part(suffix), igc-input[readonly]:not([disabled])::part(container) { - color: utils.color(gray, 800); + color: color(gray, 800); } igc-input[readonly]:not([disabled])::part(container) { background: $input-background; } +igc-input[readonly]:not([disabled])::part(prefix), +igc-input[readonly]:not([disabled])::part(suffix), igc-input[readonly]:not([disabled])::part(input) { - border-color: utils.color(gray, 400); + border-color: color(gray, 400); } igc-input[disabled] { @@ -58,12 +54,12 @@ igc-input[disabled] { igc-input[disabled]::part(prefix), igc-input[disabled]::part(suffix), igc-input[disabled]::part(container) { - background: utils.color(gray, 100); - color: utils.color(gray, 400); + background: color(gray, 100); + color: color(gray, 400); } igc-input[disabled]::part(prefix), igc-input[disabled]::part(suffix), igc-input[disabled]::part(input) { - border-color: utils.color(gray, 300); + border-color: color(gray, 300); } diff --git a/src/components/select/themes/light/select.material.scss b/src/components/select/themes/light/select.material.scss index bff40e60d..1ffa4cbb0 100644 --- a/src/components/select/themes/light/select.material.scss +++ b/src/components/select/themes/light/select.material.scss @@ -1,13 +1,13 @@ -@use '../../../../styles/utilities' as utils; +@use '../../../../styles/utilities' as *; -$idle-color: utils.color(gray, 600) !default; -$idle-hover-color: utils.color(gray, 800) !default; -$hover-background: utils.color(gray, 200) !default; +$idle-color: color(gray, 600) !default; +$idle-hover-color: color(gray, 800) !default; +$hover-background: color(gray, 200) !default; $focus-background: var(--focus-background, color(gray, 300)) !default; -$active-color: utils.color(primary, 500) !default; -$error-color: utils.color(error, 500) !default; -$idle-border-width: utils.rem(1px) !default; -$active-border-width: utils.rem(2px) !default; +$active-color: color(primary, 500) !default; +$error-color: color(error, 500) !default; +$idle-border-width: rem(1px) !default; +$active-border-width: rem(2px) !default; :host([outlined]:focus-within) { igc-input[readonly]:not([disabled])::part(start) { @@ -102,15 +102,3 @@ $active-border-width: utils.rem(2px) !default; color: $error-color; } } - -:host(:not([outlined]):focus-within) { - igc-input[readonly]:not([disabled])::part(container) { - background: var(--focus-background, utils.color(gray, 300)); - border-bottom: utils.color(primary, 500); - - &::after { - transform: scaleX(1); - opacity: 1; - } - } -} From e724d41c5035f86e76eedfb9e14a1dcb56b8b69f Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 1 Dec 2022 17:37:39 +0200 Subject: [PATCH 44/82] theme(combo): minor fixes --- src/components/combo/combo.ts | 1 - src/components/combo/themes/light/combo.base.scss | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 42e8321e9..882f262dc 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -413,7 +413,6 @@ export default class IgcComboComponent return html` this.toggle(true)} diff --git a/src/components/combo/themes/light/combo.base.scss b/src/components/combo/themes/light/combo.base.scss index 7b8719e20..34ececda7 100644 --- a/src/components/combo/themes/light/combo.base.scss +++ b/src/components/combo/themes/light/combo.base.scss @@ -9,6 +9,10 @@ > igc-input::part(input) { cursor: pointer; + + &::selection { + background: transparent; + } } } From c306f278f979d7090c6996cdbd03b9394cfac845 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 1 Dec 2022 18:01:57 +0200 Subject: [PATCH 45/82] fix(combo): cast data to ComboRecort array --- src/components/combo/combo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 882f262dc..fc5cc7aaa 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -190,7 +190,7 @@ export default class IgcComboComponent @watch('data') protected dataChanged() { - this.dataState = structuredClone(this.data); + this.dataState = structuredClone(this.data) as ComboRecord[]; if (this.hasUpdated) { this.pipeline(); From 892d13222f0f9dc79cd3c47fc1d606bb0ae80e8f Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 1 Dec 2022 19:04:28 +0200 Subject: [PATCH 46/82] refactor(combo): document items --- src/components/combo/combo.ts | 130 +++++++++++++++++++++++++--------- stories/combo.stories.ts | 19 ++++- 2 files changed, 116 insertions(+), 33 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index fc5cc7aaa..46b5529d3 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -97,7 +97,7 @@ export default class IgcComboComponent protected inputPrefix!: Array; @query('[part="search-input"]') - public input!: IgcInputComponent; + private input!: IgcInputComponent; @query('[part="target"]') private target!: IgcInputComponent; @@ -105,7 +105,7 @@ export default class IgcComboComponent @query('igc-combo-list') private list!: IgcComboListComponent; - /** The data source used to build the list of options. */ + /** The data source used to generate the list of options. */ @property({ attribute: false }) public data: Array = []; @@ -133,7 +133,10 @@ export default class IgcComboComponent @property({ type: Boolean }) public override autofocus!: boolean; - /** Focuses the first item in the list of options when the menu opens.*/ + /** + * Focuses the first item in the list of options when the menu opens. + * @attr autofocus-options + */ @property({ attribute: 'autofocus-options', type: Boolean }) public autofocusOptions = false; @@ -145,7 +148,10 @@ export default class IgcComboComponent @property({ type: String }) public placeholder!: string; - /** The placeholder attribute of the search input. */ + /** + * The placeholder attribute of the search input. + * @attr placeholder-search + */ @property({ attribute: 'placeholder-search', type: String }) public placeholderSearch = 'Search'; @@ -157,18 +163,42 @@ export default class IgcComboComponent @property({ type: Boolean }) public open = false; + /** + * The key in the data source used when selecting items. + * @attr value-key + */ @property({ attribute: 'value-key', reflect: false }) public valueKey?: Keys; + /** + * The key in the data source used to display items in the list. + * @attr display-key + */ @property({ attribute: 'display-key', reflect: false }) public displayKey?: Keys = this.valueKey; + /** + * The key in the data source used to group items in the list. + * @attr group-key + */ @property({ attribute: 'group-key', reflect: false }) public groupKey?: Keys = this.displayKey; + /** + * Sorts the items in each group by ascending or descending order. + * @attr group-sorting + * @type {"asc" | "desc"} + */ @property({ attribute: 'group-sorting', reflect: false }) public groupSorting: GroupingDirection = 'asc'; + /** + * An object that configures the filtering of the combo. + * @attr filtering-options + * @type {FilteringOptions} + * @param filterKey - The key in the data source used when filtering the list of options. + * @param caseSensitive - Determines whether the filtering operation should be case sensitive. + */ @property({ attribute: 'filtering-options', reflect: false, @@ -179,12 +209,49 @@ export default class IgcComboComponent caseSensitive: false, }; + /** + * Enables the case sensitive search icon in the filtering input. + * @attr case-sensitive-icon + */ @property({ type: Boolean, attribute: 'case-sensitive-icon', reflect: false }) public caseSensitiveIcon = false; + /** + * Disables the filtering of the list of options. + * @attr disable-filtering + */ @property({ type: Boolean, attribute: 'disable-filtering', reflect: false }) public disableFiltering = false; + /** + * The template used for the content of each combo item. + * @type {(item: T) => TemplateResult} + */ + @property({ attribute: false }) + public itemTemplate: (item: ComboRecord) => TemplateResult = (item) => { + if (typeof item !== 'object' || item === null) { + return String(item) as any; + } + + if (this.displayKey) { + return html`${item[this.displayKey]}`; + } + + return html`${String(item)}`; + }; + + /** + * The template used for the content of each combo group header. + * @type {(item: T) => TemplateResult} + */ + @property({ attribute: false }) + public groupHeaderTemplate: (item: ComboRecord) => TemplateResult = ( + item: ComboRecord + ) => { + return html`${this.groupKey && item[this.groupKey]}`; + }; + + /** @hidden @internal */ @state() public dataState: Array> = []; @@ -209,32 +276,13 @@ export default class IgcComboComponent } @watch('groupKey') + @watch('groupSorting') @watch('pipeline') protected async pipeline() { this.dataState = await this.dataController.apply([...this.data]); this.navigationController.active = 0; } - @property({ attribute: false }) - public itemTemplate: (item: ComboRecord) => TemplateResult = (item) => { - if (typeof item !== 'object' || item === null) { - return String(item) as any; - } - - if (this.displayKey) { - return html`${item[this.displayKey]}`; - } - - return html`${String(item)}`; - }; - - @property({ attribute: false }) - public groupHeaderTemplate: (item: ComboRecord) => TemplateResult = ( - item: ComboRecord - ) => { - return html`${this.groupKey && item[this.groupKey]}`; - }; - constructor() { super(); @@ -257,17 +305,21 @@ export default class IgcComboComponent ); } + protected override async firstUpdated() { + await this.updateComplete; + this.requestUpdate(); + } + + /** + * Returns the current selection as a list of commma separated values, + * represented by the display key, when provided. + */ public get value() { return this.selectionController.getValue( Array.from(this.selectionController.selected) ); } - public override async firstUpdated() { - await this.updateComplete; - this.requestUpdate(); - } - /** Sets focus on the component. */ public override focus(options?: FocusOptions) { this.target.focus(options); @@ -278,10 +330,20 @@ export default class IgcComboComponent this.target.blur(); } + /** + * Selects the options in the list by either value or key value. + * If not argument is provided all items will be selected. + * @param { T[] | Values[] } items - A list of values or values as set by the valueKey. + */ public select(items?: T[] | Values[], emit = false) { this.selectionController.select(items, emit); } + /** + * Deselects the options in the list by either value or key value. + * If not argument is provided all items will be deselected. + * @param { T[] | Values[] } items - A list of values or values as set by the valueKey. + */ public deselect(items?: T[] | Values[], emit = false) { this.selectionController.deselect(items, emit); } @@ -300,6 +362,7 @@ export default class IgcComboComponent return this.emitEvent('igcClosing', args); } + /** Shows the list of options. */ public async show(emit = false) { if (this.open) return; if (emit && !this.handleOpening()) return; @@ -315,6 +378,7 @@ export default class IgcComboComponent } } + /** Hides the list of options. */ public async hide(emit = false) { if (!this.open) return; if (emit && !this.handleClosing()) return; @@ -324,6 +388,7 @@ export default class IgcComboComponent emit && this.emitEvent('igcClosed'); } + /** Toggles the list of options. */ public toggle(emit = false) { this.open ? this.hide(emit) : this.show(emit); } @@ -357,7 +422,7 @@ export default class IgcComboComponent : itemTemplate}`; }; - protected keydownHandler(event: KeyboardEvent) { + protected listKeydownHandler(event: KeyboardEvent) { const target = event .composedPath() .find( @@ -380,6 +445,7 @@ export default class IgcComboComponent this.input.focus(); } + /** @internal @hidden */ public toggleSelect(index: number) { this.selectionController.changeSelection(index); this.navigationController.active = index; @@ -408,7 +474,7 @@ export default class IgcComboComponent return this.inputSuffix.length > 0; } - public override render() { + protected override render() { const { selected } = this.selectionController; return html` @@ -463,7 +529,7 @@ export default class IgcComboComponent
diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index a5148c4bf..7042cd5e3 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -83,18 +83,33 @@ const metadata = { control: 'boolean', defaultValue: false, }, + groupSorting: { + type: '"asc" | "desc"', + description: + 'Sorts the items in each group by ascending or descending order.', + options: ['asc', 'desc'], + control: { + type: 'inline-radio', + }, + defaultValue: 'asc', + }, caseSensitiveIcon: { type: 'boolean', + description: + 'Enables the case sensitive search icon in the filtering input.', control: 'boolean', defaultValue: false, }, disableFiltering: { type: 'boolean', + description: 'Disables the filtering of the list of options.', control: 'boolean', defaultValue: false, }, value: { type: 'string', + description: + 'Returns the current selection as a list of commma separated values,\nrepresented by the display key, when provided.', control: 'text', }, }, @@ -113,6 +128,7 @@ interface ArgTypes { placeholderSearch: string; dir: 'ltr' | 'rtl' | 'auto'; open: boolean; + groupSorting: 'asc' | 'desc'; caseSensitiveIcon: boolean; disableFiltering: boolean; value: string; @@ -231,6 +247,7 @@ const Template: Story = ( required = false, autofocus = false, autofocusOptions, + groupSorting = 'asc', }: ArgTypes, { globals: { direction } }: Context ) => html` @@ -247,7 +264,7 @@ const Template: Story = ( value-key="id" display-key="name" group-key="country" - group-sorting="asc" + group-sorting="${ifDefined(groupSorting)}" ?case-sensitive-icon=${ifDefined(caseSensitiveIcon)} ?disable-filtering=${ifDefined(disableFiltering)} ?open=${open} From 9459315e658f3d3a16aa02a6a18845f3c76654fb Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 1 Dec 2022 19:58:17 +0200 Subject: [PATCH 47/82] feat(combo): add validation --- src/components/combo/combo.ts | 16 + src/components/combo/controllers/selection.ts | 1 + src/components/form/form.ts | 1 + stories/form.stories.ts | 291 ++++++++++-------- 4 files changed, 181 insertions(+), 128 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 46b5529d3..009e16bc9 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -320,6 +320,22 @@ export default class IgcComboComponent ); } + @watch('value') + protected validate() { + this.updateComplete.then(() => this.reportValidity()); + } + + /** Checks the validity of the control. */ + public reportValidity() { + this.invalid = this.required && !this.value; + return !this.invalid; + } + + /** A wrapper around the reportValidity method to comply with the native input API. */ + public checkValidity() { + return this.reportValidity(); + } + /** Sets focus on the component. */ public override focus(options?: FocusOptions) { this.target.focus(options); diff --git a/src/components/combo/controllers/selection.ts b/src/components/combo/controllers/selection.ts index fadf9df52..75b80e550 100644 --- a/src/components/combo/controllers/selection.ts +++ b/src/components/combo/controllers/selection.ts @@ -26,6 +26,7 @@ export class SelectionController } private handleChange(detail: IgcComboChangeEventArgs) { + this.host.requestUpdate('value'); return this.host.emitEvent('igcChange', { cancelable: true, detail }); } diff --git a/src/components/form/form.ts b/src/components/form/form.ts index 8b0287f6c..8b114c202 100644 --- a/src/components/form/form.ts +++ b/src/components/form/form.ts @@ -47,6 +47,7 @@ export default class IgcFormComponent extends EventEmitterMixin< 'textarea', 'igc-rating', 'igc-select', + 'igc-combo', 'igc-date-time-input', ]; private _controlsThatSubmit = [ diff --git a/stories/form.stories.ts b/stories/form.stories.ts index bfc917941..713f7a934 100644 --- a/stories/form.stories.ts +++ b/stories/form.stories.ts @@ -55,137 +55,172 @@ const Template: Story = ({ }: ArgTypes) => { const radios = ['Male', 'Female']; const minDate = new Date(2020, 2, 3); + const comboData = [ + { make: 'Volvo', group: 'Swedish cars' }, + { make: 'Saab', group: 'Swedish cars' }, + { make: 'Mercedes', group: 'German cars' }, + { make: 'Audi', group: 'German cars' }, + ]; return html` - - -

- - -
- - - ${radios.map( - (v) => - html`${v}` - )} - -
- - -

- - - - - Swedish Cars - Volvo - Saab - - - German Cars - Mercedes - Audi - - -

- - - - - - - - - -

-
- Preferred soical media handle: - - - - - - -
-
-
- Subscribe to newsletter -
- -

- - - - - - - - - Check if you think this is a long form - Reset - Submit -
+ > +

+ + +

+
+ + + ${radios.map( + (v) => + html`${v}` + )} + +
+

+ + +

+ + +

+ + + Swedish Cars + Volvo + Saab + + + German Cars + Mercedes + Audi + + +

+ + +

+ + + + + + + + + +

+
+ Preferred soical media handle: + + + + + + +
+
+
+ Subscribe to newsletter +
+ +

+ + + + + + + + + Check if you think this is a long form + Reset + Submit + + `; }; From d198e2029f442d156a7a0972ca285371d793914f Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 1 Dec 2022 21:25:50 +0200 Subject: [PATCH 48/82] fix(combo): toggle position and size --- src/components/combo/combo.ts | 26 ++++++++++++++++++- .../combo/themes/light/combo.base.scss | 7 ++--- stories/combo.stories.ts | 12 +++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 009e16bc9..5eaad7e1d 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -163,6 +163,14 @@ export default class IgcComboComponent @property({ type: Boolean }) public open = false; + /** @hidden @internal */ + @property({ type: Boolean }) + public flip = true; + + /** @hidden @internal */ + @property({ type: Boolean, attribute: 'same-width' }) + public sameWidth = true; + /** * The key in the data source used when selecting items. * @attr value-key @@ -283,6 +291,13 @@ export default class IgcComboComponent this.navigationController.active = 0; } + @watch('open') + protected toggleDirectiveChange() { + if (!this.target) return; + this.toggleController.target = this.target; + this.target.setAttribute('aria-expanded', this.open ? 'true' : 'false'); + } + constructor() { super(); @@ -310,6 +325,12 @@ export default class IgcComboComponent this.requestUpdate(); } + protected override async getUpdateComplete() { + const result = await super.getUpdateComplete(); + await this.toggleController.rendered; + return result; + } + /** * Returns the current selection as a list of commma separated values, * represented by the display key, when provided. @@ -497,7 +518,10 @@ export default class IgcComboComponent this.toggle(true)} + @click=${(e: MouseEvent) => { + e.preventDefault(); + this.toggle(true); + }} value=${ifDefined(this.value)} placeholder=${ifDefined(this.placeholder)} label=${ifDefined(this.label)} diff --git a/src/components/combo/themes/light/combo.base.scss b/src/components/combo/themes/light/combo.base.scss index 34ececda7..68143dad3 100644 --- a/src/components/combo/themes/light/combo.base.scss +++ b/src/components/combo/themes/light/combo.base.scss @@ -6,8 +6,10 @@ display: block; font-family: var(--ig-font-family); + z-index: 1; > igc-input::part(input) { + text-overflow: ellipsis; cursor: pointer; &::selection { @@ -16,10 +18,6 @@ } } -igc-input::part(helper-text) { - position: absolute; -} - :host([disabled]) { igc-input::part(label), igc-input::part(input)::placeholder, @@ -42,7 +40,6 @@ igc-input::part(helper-text) { @include border-radius(rem(4px)); position: absolute; - width: 100%; background: color(surface); color: color(gray, 700); box-shadow: elevation(8); diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 7042cd5e3..e75c06412 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -83,6 +83,16 @@ const metadata = { control: 'boolean', defaultValue: false, }, + flip: { + type: 'boolean', + control: 'boolean', + defaultValue: true, + }, + sameWidth: { + type: 'boolean', + control: 'boolean', + defaultValue: true, + }, groupSorting: { type: '"asc" | "desc"', description: @@ -128,6 +138,8 @@ interface ArgTypes { placeholderSearch: string; dir: 'ltr' | 'rtl' | 'auto'; open: boolean; + flip: boolean; + sameWidth: boolean; groupSorting: 'asc' | 'desc'; caseSensitiveIcon: boolean; disableFiltering: boolean; From 5fc271b12a14050b680f860d0b602e68eba592a8 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 1 Dec 2022 21:29:20 +0200 Subject: [PATCH 49/82] theme(combo): fix z-index --- src/components/combo/themes/light/combo.base.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/combo/themes/light/combo.base.scss b/src/components/combo/themes/light/combo.base.scss index 68143dad3..b52a0c8c3 100644 --- a/src/components/combo/themes/light/combo.base.scss +++ b/src/components/combo/themes/light/combo.base.scss @@ -6,7 +6,6 @@ display: block; font-family: var(--ig-font-family); - z-index: 1; > igc-input::part(input) { text-overflow: ellipsis; @@ -45,6 +44,7 @@ box-shadow: elevation(8); overflow: hidden; outline-style: none; + z-index: 999999; igc-input { --ig-size: var(--ig-size-small); From 9052282b6f5bb8928880916da04984cf660210dd Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 2 Dec 2022 07:37:02 +0200 Subject: [PATCH 50/82] fix(combo): focus target when hiding --- src/components/combo/combo.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 5eaad7e1d..e0494d966 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -303,14 +303,20 @@ export default class IgcComboComponent this.toggleController = new IgcToggleController(this, { target: this.target, + closeCallback: async () => { + if (!this.handleClosing()) return; + this.open = false; + + await this.updateComplete; + this.emitEvent('igcClosed'); + }, }); this.addEventListener('focus', () => { this.emitEvent('igcFocus'); }); - this.addEventListener('blur', async () => { - await this.hide(true); + this.addEventListener('blur', () => { this.emitEvent('igcBlur'); }); @@ -423,6 +429,7 @@ export default class IgcComboComponent await this.updateComplete; emit && this.emitEvent('igcClosed'); + this.target.focus(); } /** Toggles the list of options. */ From 639a1184fcb9c56d54b2e0ccbeaee77f07a26986 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 2 Dec 2022 08:01:45 +0200 Subject: [PATCH 51/82] refactor(combo): improve accessibility --- src/components/combo/combo-list.ts | 1 + src/components/combo/combo.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/combo/combo-list.ts b/src/components/combo/combo-list.ts index a6a9ec27a..8110f5cb9 100644 --- a/src/components/combo/combo-list.ts +++ b/src/components/combo/combo-list.ts @@ -7,6 +7,7 @@ export default class IgcComboListComponent extends LitVirtualizer { public override connectedCallback() { super.connectedCallback(); this.setAttribute('tabindex', '0'); + this.setAttribute('role', 'listbox'); } } diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index e0494d966..67e3447dc 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -99,7 +99,7 @@ export default class IgcComboComponent @query('[part="search-input"]') private input!: IgcInputComponent; - @query('[part="target"]') + @query('[part="input"]') private target!: IgcInputComponent; @query('igc-combo-list') @@ -523,7 +523,12 @@ export default class IgcComboComponent return html` { e.preventDefault(); @@ -606,6 +611,7 @@ export default class IgcComboComponent
Date: Fri, 2 Dec 2022 08:12:16 +0200 Subject: [PATCH 52/82] docs(combo): document the exposed parts --- src/components/combo/combo.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 67e3447dc..b5f9d066c 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -64,13 +64,24 @@ defineComponents( * @fires igcClosing - Emitter just before the list of options is closed. * @fires igcClosed - Emitted after the list of options is closed. * - * @csspart list - The list of options wrapper. - * @csspart input - The encapsulated igc-input. * @csspart label - The encapsulated text label. + * @csspart input - The main input field. * @csspart prefix - The prefix wrapper. * @csspart suffix - The suffix wrapper. * @csspart toggle-icon - The toggle icon wrapper. + * @csspart clear-icon - The clear icon wrapper. * @csspart helper-text - The helper text wrapper. + * @csspart search-input - The search input field. + * @csspart list-wrapper - The list of options wrapper. + * @csspart list - The list of options box. + * @csspart item - Represents each item in the list of options. + * @csspart group-header - Represents each header in the list of options. + * @csspart active - Appended to the item parts list when the item is active. + * @csspart selected - Appended to the item parts list when the item is selected. + * @csspart checkbox - Represents each checkbox of each list item. + * @csspart checkbox-indicator - Represents the checkbox indicator of each list item. + * @csspart checked - Appended to checkbox parts list when checkbox is checked. + * @csspart empty - The container holding the empty content. */ @themes({ material, bootstrap, fluent, indigo }) export default class IgcComboComponent From 52a86d4310a2ee548a137f586092390148974e23 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 2 Dec 2022 09:03:43 +0200 Subject: [PATCH 53/82] refactor(combo): few changes --- src/components/combo/combo.ts | 10 +- .../common/definitions/defineAllComponents.ts | 2 - src/index.ts | 1 - stories/combo.stories.ts | 11 +- stories/form.stories.ts | 314 +++++++++--------- stories/linear-progress.stories.ts | 6 +- stories/nav-drawer.stories.ts | 6 +- stories/stepper.stories.ts | 6 +- 8 files changed, 176 insertions(+), 180 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index b5f9d066c..457c305a8 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -145,11 +145,11 @@ export default class IgcComboComponent public override autofocus!: boolean; /** - * Focuses the first item in the list of options when the menu opens. - * @attr autofocus-options + * Focuses the list of options when the menu opens. + * @attr autofocus-list */ - @property({ attribute: 'autofocus-options', type: Boolean }) - public autofocusOptions = false; + @property({ attribute: 'autofocus-list', type: Boolean }) + public autofocusList = false; /** The label attribute of the control. */ @property({ type: String }) @@ -427,7 +427,7 @@ export default class IgcComboComponent this.list.focus(); - if (!this.autofocusOptions) { + if (!this.autofocusList) { this.input.focus(); } } diff --git a/src/components/common/definitions/defineAllComponents.ts b/src/components/common/definitions/defineAllComponents.ts index b8b7f0ae2..d1e1b69f3 100644 --- a/src/components/common/definitions/defineAllComponents.ts +++ b/src/components/common/definitions/defineAllComponents.ts @@ -13,7 +13,6 @@ import IgcCheckboxComponent from '../../checkbox/checkbox.js'; import IgcChipComponent from '../../chip/chip.js'; import IgcCircularProgressComponent from '../../progress/circular-progress.js'; import IgcComboComponent from '../../combo/combo.js'; -import IgcComboItemComponent from '../../combo/combo-item.js'; import IgcDropdownComponent from '../../dropdown/dropdown.js'; import IgcDropdownGroupComponent from '../../dropdown/dropdown-group.js'; import IgcDropdownHeaderComponent from '../../dropdown/dropdown-header.js'; @@ -68,7 +67,6 @@ const allComponents: CustomElementConstructor[] = [ IgcCheckboxComponent, IgcChipComponent, IgcComboComponent, - IgcComboItemComponent, IgcDropdownComponent, IgcDropdownGroupComponent, IgcDropdownHeaderComponent, diff --git a/src/index.ts b/src/index.ts index f863fc257..42e1b5002 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,6 @@ export { default as IgcCircularProgressComponent } from './components/progress/c export { default as IgcCircularGradientComponent } from './components/progress/circular-gradient.js'; export { default as IgcChipComponent } from './components/chip/chip.js'; export { default as IgcComboComponent } from './components/combo/combo.js'; -export { default as IgcComboItemComponent } from './components/combo/combo-item.js'; export { default as IgcDateTimeInputComponent } from './components/date-time-input/date-time-input.js'; export { default as IgcDialogComponent } from './components/dialog/dialog.js'; export { default as IgcDropdownComponent } from './components/dropdown/dropdown.js'; diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index e75c06412..e2d7495be 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -45,10 +45,9 @@ const metadata = { description: 'The autofocus attribute of the control.', control: 'boolean', }, - autofocusOptions: { + autofocusList: { type: 'boolean', - description: - 'Focuses the first item in the list of options when the menu opens.', + description: 'Focuses the list of options when the menu opens.', control: 'boolean', defaultValue: false, }, @@ -132,7 +131,7 @@ interface ArgTypes { invalid: boolean; outlined: boolean; autofocus: boolean; - autofocusOptions: boolean; + autofocusList: boolean; label: string; placeholder: string; placeholderSearch: string; @@ -258,7 +257,7 @@ const Template: Story = ( invalid = false, required = false, autofocus = false, - autofocusOptions, + autofocusList, groupSorting = 'asc', }: ArgTypes, { globals: { direction } }: Context @@ -281,7 +280,7 @@ const Template: Story = ( ?disable-filtering=${ifDefined(disableFiltering)} ?open=${open} ?autofocus=${autofocus} - ?autofocus-options=${ifDefined(autofocusOptions)} + ?autofocus-list=${ifDefined(autofocusList)} ?outlined=${outlined} ?required=${required} ?disabled=${disabled} diff --git a/stories/form.stories.ts b/stories/form.stories.ts index 713f7a934..1a3f9d956 100644 --- a/stories/form.stories.ts +++ b/stories/form.stories.ts @@ -63,164 +63,164 @@ const Template: Story = ({ ]; return html` - - -

- - -

-
- - - ${radios.map( - (v) => - html`${v}` - )} - -
-

- - -

- - -

- - - Swedish Cars - Volvo - Saab - - - German Cars - Mercedes - Audi - - -

- - -

- - - - - - - - - -

-
- Preferred soical media handle: - - - - - - -
-
-
- Subscribe to newsletter -
- -

- - - - - - - - - Check if you think this is a long form - Reset - Submit -
- + > +

+ + +

+
+ + + ${radios.map( + (v) => + html`${v}` + )} + +
+

+ + +

+ + +

+ + + Swedish Cars + Volvo + Saab + + + German Cars + Mercedes + Audi + + +

+ + +

+ + + + + + + + + +

+
+ Preferred soical media handle: + + + + + + +
+
+
+ Subscribe to newsletter +
+ +

+ + + + + + + + + Check if you think this is a long form + Reset + Submit + + `; }; diff --git a/stories/linear-progress.stories.ts b/stories/linear-progress.stories.ts index 4d4003b54..37147ca89 100644 --- a/stories/linear-progress.stories.ts +++ b/stories/linear-progress.stories.ts @@ -14,13 +14,13 @@ const metadata = { defaultValue: false, }, labelAlign: { - type: '"top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end"', + type: '"top" | "bottom" | "top-start" | "top-end" | "bottom-start" | "bottom-end"', description: 'The position for the default label of the control.', options: [ 'top', + 'bottom', 'top-start', 'top-end', - 'bottom', 'bottom-start', 'bottom-end', ], @@ -81,9 +81,9 @@ interface ArgTypes { striped: boolean; labelAlign: | 'top' + | 'bottom' | 'top-start' | 'top-end' - | 'bottom' | 'bottom-start' | 'bottom-end'; max: number; diff --git a/stories/nav-drawer.stories.ts b/stories/nav-drawer.stories.ts index 47928546b..1fcd44903 100644 --- a/stories/nav-drawer.stories.ts +++ b/stories/nav-drawer.stories.ts @@ -13,9 +13,9 @@ const metadata = { component: 'igc-nav-drawer', argTypes: { position: { - type: '"top" | "bottom" | "start" | "end" | "relative"', + type: '"start" | "end" | "top" | "bottom" | "relative"', description: 'The position of the drawer.', - options: ['top', 'bottom', 'start', 'end', 'relative'], + options: ['start', 'end', 'top', 'bottom', 'relative'], control: { type: 'select', }, @@ -31,7 +31,7 @@ const metadata = { }; export default metadata; interface ArgTypes { - position: 'top' | 'bottom' | 'start' | 'end' | 'relative'; + position: 'start' | 'end' | 'top' | 'bottom' | 'relative'; open: boolean; } // endregion diff --git a/stories/stepper.stories.ts b/stories/stepper.stories.ts index ff51e2a4c..7f48f399d 100644 --- a/stories/stepper.stories.ts +++ b/stories/stepper.stories.ts @@ -38,9 +38,9 @@ const metadata = { defaultValue: false, }, titlePosition: { - type: '"top" | "bottom" | "start" | "end" | undefined', + type: '"start" | "end" | "top" | "bottom" | undefined', description: 'Get/Set the position of the steps title.', - options: ['top', 'bottom', 'start', 'end', 'undefined'], + options: ['start', 'end', 'top', 'bottom', 'undefined'], control: { type: 'select', }, @@ -62,7 +62,7 @@ interface ArgTypes { stepType: 'indicator' | 'title' | 'full'; linear: boolean; contentTop: boolean; - titlePosition: 'top' | 'bottom' | 'start' | 'end' | undefined; + titlePosition: 'start' | 'end' | 'top' | 'bottom' | undefined; size: 'small' | 'medium' | 'large'; } // endregion From b0dfbceb9edcfeb0040ae175b6252b78c9500ada Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 2 Dec 2022 11:42:30 +0200 Subject: [PATCH 54/82] fix(combo): accessibility and navigation --- src/components/combo/combo-item.ts | 3 ++- src/components/combo/combo.ts | 4 +++- src/components/combo/controllers/navigation.ts | 9 ++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/combo/combo-item.ts b/src/components/combo/combo-item.ts index 5e1004fea..b14bf935c 100644 --- a/src/components/combo/combo-item.ts +++ b/src/components/combo/combo-item.ts @@ -47,12 +47,13 @@ export default class IgcComboItemComponent extends LitElement { return html`
-
+
`; diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 457c305a8..a477d93c0 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -107,8 +107,9 @@ export default class IgcComboComponent @queryAssignedElements({ slot: 'prefix' }) protected inputPrefix!: Array; + /** @hidden @internal */ @query('[part="search-input"]') - private input!: IgcInputComponent; + public input!: IgcInputComponent; @query('[part="input"]') private target!: IgcInputComponent; @@ -624,6 +625,7 @@ export default class IgcComboComponent protected searchInputHandlers = new Map( Object.entries({ Escape: this.escape, + ArrowUp: this.escape, ArrowDown: this.inputArrowDown, Tab: this.inputArrowDown, }) @@ -34,6 +35,7 @@ export class NavigationController ' ': this.space, Enter: this.enter, Escape: this.escape, + Tab: this.escape, Home: this.home, End: this.end, }) @@ -121,7 +123,12 @@ export class NavigationController protected getNextItem(direction: DIRECTION) { const next = this.getNearestItem(this.currentItem, direction); - if (next === -1) return; + if (next === -1) { + if (this.active === this.lastItem) return; + this.host.input.focus(); + return; + } + this.active = next; } From d82f47aaaf6d6dd82ad62629bc59d47d24c111fc Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 2 Dec 2022 11:44:41 +0200 Subject: [PATCH 55/82] spec(combo): initial commit --- src/components/combo/combo.spec.ts | 130 +++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/src/components/combo/combo.spec.ts b/src/components/combo/combo.spec.ts index e69de29bb..8eeb864e8 100644 --- a/src/components/combo/combo.spec.ts +++ b/src/components/combo/combo.spec.ts @@ -0,0 +1,130 @@ +import { html } from 'lit'; +// import sinon from 'sinon'; +import { elementUpdated, expect, fixture } from '@open-wc/testing'; +import { defineComponents } from '../common/definitions/defineComponents.js'; +// import IgcInputComponent from '../input/input'; +import IgcComboComponent from './combo.js'; +// import IgcComboItemComponent from './combo-item.js'; +// import IgcComboListComponent from './combo-list.js'; +// import IgcComboHeaderComponent from './combo-header.js'; + +describe('Combo Component', () => { + interface City { + id: string; + name: string; + country: string; + zip: string; + } + + // let input: IgcInputComponent; + // let searchInput: IgcInputComponent; + + const cities: City[] = [ + { + id: 'BG01', + name: 'Sofia', + country: 'Bulgaria', + zip: '1000', + }, + { + id: 'BG02', + name: 'Plovdiv', + country: 'Bulgaria', + zip: '4000', + }, + { + id: 'BG03', + name: 'Varna', + country: 'Bulgaria', + zip: '9000', + }, + { + id: 'US01', + name: 'New York', + country: 'United States', + zip: '10001', + }, + { + id: 'US02', + name: 'Boston', + country: 'United States', + zip: '02108', + }, + { + id: 'US03', + name: 'San Francisco', + country: 'United States', + zip: '94103', + }, + { + id: 'JP01', + name: 'Tokyo', + country: 'Japan', + zip: '163-8001', + }, + { + id: 'JP02', + name: 'Yokohama', + country: 'Japan', + zip: '781-0240', + }, + { + id: 'JP03', + name: 'Osaka', + country: 'Japan', + zip: '552-0021', + }, + ]; + + let combo: IgcComboComponent; + // const comboItems = (el: IgcComboItemComponent) => + // [...el.querySelectorAll('igc-combo-item')] as IgcComboItemComponent[]; + + before(() => { + defineComponents(IgcComboComponent); + }); + + describe('', () => { + beforeEach(async () => { + combo = await fixture>( + html`` + ); + + // input = combo.shadowRoot!.querySelector( + // '[part="input"]' + // ) as IgcInputComponent; + // searchInput = combo.shadowRoot!.querySelector( + // '[part="search-input"]' + // ) as IgcInputComponent; + }); + + it('is accessible.', async () => { + combo.open = true; + combo.label = 'Simple Select'; + await elementUpdated(combo); + await expect(combo).to.be.accessible(); + }); + }); +}); +// const pressKey = ( +// target: HTMLElement, +// key: string, +// times = 1, +// options?: Object +// ) => { +// for (let i = 0; i < times; i++) { +// target.dispatchEvent( +// new KeyboardEvent('keydown', { +// key: key, +// bubbles: true, +// composed: true, +// ...options, +// }) +// ); +// } +// }; From 72501df2cf436731aedc1b822d688d18723bf1b1 Mon Sep 17 00:00:00 2001 From: Marin Popov Date: Fri, 2 Dec 2022 11:52:52 +0200 Subject: [PATCH 56/82] Fix combo styling bugs (#571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style(combo): -- Combo Fluent - Input invalid jumps on focus -- Combo Fluent - Input has red border when disabled, outlined and invalid -- Combo Bootstrap - The clear-icon border doesn’t change color when disabled * style(combo) fix material prefix bug. Co-authored-by: Simeon Simeonoff --- .../combo/themes/light/combo.bootstrap.scss | 6 ++++++ .../combo/themes/light/combo.fluent.scss | 17 +++++++++++++++++ .../input/themes/light/input.fluent.scss | 6 ++++++ 3 files changed, 29 insertions(+) diff --git a/src/components/combo/themes/light/combo.bootstrap.scss b/src/components/combo/themes/light/combo.bootstrap.scss index d1efdc832..600a810e8 100644 --- a/src/components/combo/themes/light/combo.bootstrap.scss +++ b/src/components/combo/themes/light/combo.bootstrap.scss @@ -26,3 +26,9 @@ } } } + +:host([disabled]) { + [part='clear-icon'] { + border-inline-end: 1px solid color(gray, 300); + } +} diff --git a/src/components/combo/themes/light/combo.fluent.scss b/src/components/combo/themes/light/combo.fluent.scss index 079f2e468..f53aef0f6 100644 --- a/src/components/combo/themes/light/combo.fluent.scss +++ b/src/components/combo/themes/light/combo.fluent.scss @@ -12,6 +12,23 @@ } } +:host([invalid]:focus-within) { + igc-input::part(input) { + height: calc(var(--size) - #{rem(4px)}); + } + + igc-input::part(suffix) { + height: var(--size); + margin-inline-end: rem(-1px); + margin-block-start: rem(-2px); + } + + igc-input::part(prefix) { + margin-inline-start: rem(-1px); + margin-block-start: rem(-2px); + } +} + [part='filter-input'] { padding: pad(2px, 4px, 8px); } diff --git a/src/components/input/themes/light/input.fluent.scss b/src/components/input/themes/light/input.fluent.scss index c884e0562..536adcf62 100644 --- a/src/components/input/themes/light/input.fluent.scss +++ b/src/components/input/themes/light/input.fluent.scss @@ -143,6 +143,12 @@ $focused-height: calc(var(--size) - #{($focused-border-width * 2)}); } } +:host([disabled][outlined][invalid]) { + [part^='container'] { + border-color: color(gray, 100); + } +} + :host([disabled]) { pointer-events: none; From 76997073ac6b38700c0f074baa032381d602faf9 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 2 Dec 2022 11:57:42 +0200 Subject: [PATCH 57/82] lint(combo-item): fix aria-hidden error --- src/components/combo/combo-item.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/combo/combo-item.ts b/src/components/combo/combo-item.ts index b14bf935c..7752f01d2 100644 --- a/src/components/combo/combo-item.ts +++ b/src/components/combo/combo-item.ts @@ -47,7 +47,7 @@ export default class IgcComboItemComponent extends LitElement { return html`
Date: Fri, 2 Dec 2022 13:33:29 +0200 Subject: [PATCH 58/82] npm(lit-virtualizer): move to dependencies --- package-lock.json | 6 ++---- package.json | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1b439a780..297ae2cca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "SEE LICENSE IN LICENSE", "dependencies": { "@floating-ui/dom": "^1.0.7", + "@lit-labs/virtualizer": "^0.7.2", "lit": "^2.4.1" }, "devDependencies": { @@ -1982,7 +1983,6 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/@lit-labs/virtualizer/-/virtualizer-0.7.2.tgz", "integrity": "sha512-P6sUc9ixaIGYU7gOV3O4ZGrO1Eo1c1sVYGwTIJdJTxXzobi4e5Di6beKksv9HzZdHkTj1JHapCj9hRcSO5kmMQ==", - "dev": true, "dependencies": { "event-target-shim": "^6.0.2", "lit": "^2.0.0", @@ -7301,7 +7301,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz", "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==", - "dev": true, "engines": { "node": ">=10.13.0" }, @@ -16086,8 +16085,7 @@ "node_modules/tslib": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "dev": true + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, "node_modules/tsscmp": { "version": "1.0.6", diff --git a/package.json b/package.json index f72d3fde8..8e2627ab2 100644 --- a/package.json +++ b/package.json @@ -54,11 +54,11 @@ }, "dependencies": { "@floating-ui/dom": "^1.0.7", + "@lit-labs/virtualizer": "^0.7.2", "lit": "^2.4.1" }, "devDependencies": { "@igniteui/material-icons-extended": "^2.11.0", - "@lit-labs/virtualizer": "^0.7.2", "@open-wc/eslint-config": "^9.0.0", "@open-wc/testing": "^3.1.7", "@storybook/storybook-deployer": "^2.8.16", From 19ddf42389ff196ba7107f83b723b2ee63dc08e6 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 2 Dec 2022 13:48:24 +0200 Subject: [PATCH 59/82] refactor(combo): sort group after grouping --- src/components/combo/operations/group.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/combo/operations/group.ts b/src/components/combo/operations/group.ts index b3dec5702..0458fb826 100644 --- a/src/components/combo/operations/group.ts +++ b/src/components/combo/operations/group.ts @@ -41,13 +41,13 @@ export default class GroupDataOperation { if (!groupKey) return data; - const result = new Map(); + const groups = new Map(); data.forEach((item: T) => { if (typeof item !== 'object' || item === null) return; const key = item[groupKey!] ?? 'Other'; - const group = result.get(key) ?? >[]; + const group = groups.get(key) ?? >[]; if (group.length === 0) { group.push({ @@ -59,17 +59,18 @@ export default class GroupDataOperation { } group.push(item); + groups.set(key, group); + }); + groups.forEach((group) => { group.sort((a: ComboRecord, b: ComboRecord) => { if (!a.header && !b.header) { return this.compareObjects(a, b, displayKey!, direction); } return 1; }); - - result.set(key, group); }); - return Array.from(result.values()).flat(); + return Array.from(groups.values()).flat(); } } From 117a99d3fa56f49ea6a7a855defee6c6e3e23d9e Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 2 Dec 2022 14:30:02 +0200 Subject: [PATCH 60/82] refactor(combo): address review comments --- src/components/combo/combo.ts | 111 +++++++++++++----- .../combo/controllers/navigation.ts | 6 +- src/components/combo/controllers/selection.ts | 10 ++ 3 files changed, 92 insertions(+), 35 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index a477d93c0..6e6fe24e2 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -36,6 +36,7 @@ import { partNameMap } from '../common/util.js'; import { filteringOptionsConverter } from './utils/converters.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import { Constructor } from '../common/mixins/constructor.js'; +import { blazorAdditionalDependencies } from '../common/decorators/blazorAdditionalDependencies.js'; defineComponents( IgcIconComponent, @@ -84,6 +85,9 @@ defineComponents( * @csspart empty - The container holding the empty content. */ @themes({ material, bootstrap, fluent, indigo }) +@blazorAdditionalDependencies( + 'IgcIconComponent, IgcComboListComponent, IgcComboItemComponent, IgcComboHeaderComponent, IgcInputComponent' +) export default class IgcComboComponent extends EventEmitterMixin>( LitElement @@ -121,27 +125,45 @@ export default class IgcComboComponent @property({ attribute: false }) public data: Array = []; - /** The name attribute of the control. */ + /** + * The name attribute of the control. + * @attr name + */ @property() public name!: string; - /** The disabled attribute of the control. */ + /** + * The disabled attribute of the control. + * @attr disabled + */ @property({ reflect: true, type: Boolean }) public disabled = false; - /** The required attribute of the control. */ + /** + * The required attribute of the control. + * @attr required + */ @property({ reflect: true, type: Boolean }) public required = false; - /** The invalid attribute of the control. */ + /** + * The invalid attribute of the control. + * @attr invalid + */ @property({ reflect: true, type: Boolean }) public invalid = false; - /** The outlined attribute of the control. */ + /** + * The outlined attribute of the control. + * @attr outlined + */ @property({ reflect: true, type: Boolean }) public outlined = false; - /** The autofocus attribute of the control. */ + /** + * The autofocus attribute of the control. + * @attr autofocus + */ @property({ type: Boolean }) public override autofocus!: boolean; @@ -152,11 +174,17 @@ export default class IgcComboComponent @property({ attribute: 'autofocus-list', type: Boolean }) public autofocusList = false; - /** The label attribute of the control. */ + /** + * The label attribute of the control. + * @attr label + */ @property({ type: String }) public label!: string; - /** The placeholder attribute of the control. */ + /** + * The placeholder attribute of the control. + * @attr placeholder + */ @property({ type: String }) public placeholder!: string; @@ -167,11 +195,17 @@ export default class IgcComboComponent @property({ attribute: 'placeholder-search', type: String }) public placeholderSearch = 'Search'; - /** The direction attribute of the control. */ + /** + * The direction attribute of the control. + * @attr dir + */ @property({ reflect: true }) public override dir: 'ltr' | 'rtl' | 'auto' = 'auto'; - /** Sets the open state of the component. */ + /** + * Sets the open state of the component. + * @attr open + */ @property({ type: Boolean }) public open = false; @@ -248,7 +282,7 @@ export default class IgcComboComponent * @type {(item: T) => TemplateResult} */ @property({ attribute: false }) - public itemTemplate: (item: ComboRecord) => TemplateResult = (item) => { + public itemTemplate: (item: T) => TemplateResult = (item) => { if (typeof item !== 'object' || item === null) { return String(item) as any; } @@ -265,9 +299,7 @@ export default class IgcComboComponent * @type {(item: T) => TemplateResult} */ @property({ attribute: false }) - public groupHeaderTemplate: (item: ComboRecord) => TemplateResult = ( - item: ComboRecord - ) => { + public groupHeaderTemplate: (item: T) => TemplateResult = (item) => { return html`${this.groupKey && item[this.groupKey]}`; }; @@ -390,8 +422,8 @@ export default class IgcComboComponent * If not argument is provided all items will be selected. * @param { T[] | Values[] } items - A list of values or values as set by the valueKey. */ - public select(items?: T[] | Values[], emit = false) { - this.selectionController.select(items, emit); + public select(items?: T[] | Values[]) { + this.selectionController.select(items, false); } /** @@ -399,8 +431,8 @@ export default class IgcComboComponent * If not argument is provided all items will be deselected. * @param { T[] | Values[] } items - A list of values or values as set by the valueKey. */ - public deselect(items?: T[] | Values[], emit = false) { - this.selectionController.deselect(items, emit); + public deselect(items?: T[] | Values[]) { + this.selectionController.deselect(items, false); } protected handleSearchInput(e: CustomEvent) { @@ -417,8 +449,8 @@ export default class IgcComboComponent return this.emitEvent('igcClosing', args); } - /** Shows the list of options. */ - public async show(emit = false) { + /** @hidden @internal */ + public async _show(emit = true) { if (this.open) return; if (emit && !this.handleOpening()) return; this.open = true; @@ -433,8 +465,13 @@ export default class IgcComboComponent } } - /** Hides the list of options. */ - public async hide(emit = false) { + /** Shows the list of options. */ + public show() { + this._show(false); + } + + /** @hidden @internal */ + public async _hide(emit = true) { if (!this.open) return; if (emit && !this.handleClosing()) return; this.open = false; @@ -444,23 +481,33 @@ export default class IgcComboComponent this.target.focus(); } + /** Hides the list of options. */ + public hide() { + this._hide(false); + } + + /** @hidden @internal */ + public _toggle(emit = true) { + this.open ? this._hide(emit) : this._show(emit); + } + /** Toggles the list of options. */ - public toggle(emit = false) { - this.open ? this.hide(emit) : this.show(emit); + public toggle() { + this._toggle(false); } protected itemRenderer = (item: T, index: number): TemplateResult => { const record = item as ComboRecord; - const { selected } = this.selectionController; - + const active = this.navigationController.active === index; + const selected = this.selectionController.selected.has(item); const headerTemplate = html`${this.groupHeaderTemplate(record)}`; const itemParts = partNameMap({ item: true, - selected: selected.has(item), - active: this.navigationController.active === index, + selected, + active, }); const itemTemplate = html` exportparts="checkbox, checkbox-indicator, checked" @click=${this.itemClickHandler.bind(this)} .index=${index} - .active=${this.navigationController.active === index} - .selected=${selected.has(item)} + .active=${active} + .selected=${selected} >${this.itemTemplate(record)}`; @@ -513,7 +560,7 @@ export default class IgcComboComponent protected handleClearIconClick(e: MouseEvent) { e.stopPropagation(); - this.deselect(); + this.selectionController.deselect([], true); this.navigationController.active = 0; } @@ -544,7 +591,7 @@ export default class IgcComboComponent exportparts="container: input, input: native-input, label, prefix, suffix" @click=${(e: MouseEvent) => { e.preventDefault(); - this.toggle(true); + this._toggle(true); }} value=${ifDefined(this.value)} placeholder=${ifDefined(this.placeholder)} diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts index 061957443..9ffa06e90 100644 --- a/src/components/combo/controllers/navigation.ts +++ b/src/components/combo/controllers/navigation.ts @@ -90,12 +90,12 @@ export class NavigationController } protected escape() { - this.host.hide(true); + this.host._hide(); } protected enter() { this.space(); - this.host.hide(true); + this.host._hide(); } protected inputArrowDown(container: IgcComboListComponent) { @@ -107,7 +107,7 @@ export class NavigationController } protected hostArrowDown() { - this.host.show(true); + this.host._show(); } protected arrowDown(container: IgcComboListComponent) { diff --git a/src/components/combo/controllers/selection.ts b/src/components/combo/controllers/selection.ts index 75b80e550..7ae7f0f00 100644 --- a/src/components/combo/controllers/selection.ts +++ b/src/components/combo/controllers/selection.ts @@ -122,6 +122,16 @@ export class SelectionController public async deselect(items?: T[] | Values[], emit = false) { if (!items || items.length === 0) { + if ( + emit && + !this.handleChange({ + newValue: '', + items: Array.from(this.selected), + type: 'deselection', + }) + ) { + return; + } this.deselectAll(); return; } From 771e60368b03f9a0170a4161094f7dca1a731916 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 2 Dec 2022 14:32:10 +0200 Subject: [PATCH 61/82] refactor(combo): update filter imports --- src/components/combo/operations/filter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/combo/operations/filter.ts b/src/components/combo/operations/filter.ts index cfeee8af3..5ef94a153 100644 --- a/src/components/combo/operations/filter.ts +++ b/src/components/combo/operations/filter.ts @@ -1,4 +1,4 @@ -import { DataController } from '../controllers/data'; +import { DataController } from '../controllers/data.js'; export default class FilterDataOperation { public apply(data: T[], controller: DataController) { From 91e7be56046c756bf363a5a61774f2744cded908 Mon Sep 17 00:00:00 2001 From: Marin Popov Date: Fri, 2 Dec 2022 14:48:21 +0200 Subject: [PATCH 62/82] style(input) fix mozzila and safari prefix and suffix. (#572) Co-authored-by: Simeon Simeonoff --- src/components/combo/themes/light/combo.base.scss | 1 + src/components/combo/themes/light/combo.indigo.scss | 2 +- src/components/combo/themes/light/combo.material.scss | 2 +- src/components/input/themes/light/input.bootstrap.scss | 3 ++- src/components/input/themes/light/input.fluent.scss | 3 ++- src/components/input/themes/light/input.indigo.scss | 4 ++-- src/components/input/themes/light/input.material.scss | 4 ++-- 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/combo/themes/light/combo.base.scss b/src/components/combo/themes/light/combo.base.scss index b52a0c8c3..85eddfcc6 100644 --- a/src/components/combo/themes/light/combo.base.scss +++ b/src/components/combo/themes/light/combo.base.scss @@ -94,6 +94,7 @@ [part='case-icon'] { color: color(gray, 600); + min-width: rem(20px); } [part='case-icon active'] { diff --git a/src/components/combo/themes/light/combo.indigo.scss b/src/components/combo/themes/light/combo.indigo.scss index 9efab5a9e..b478044b5 100644 --- a/src/components/combo/themes/light/combo.indigo.scss +++ b/src/components/combo/themes/light/combo.indigo.scss @@ -2,7 +2,7 @@ @use '../../../select/themes/light/select.indigo'; [part~='case-icon'] { - padding: rem(8px); + padding: 0; } :host { diff --git a/src/components/combo/themes/light/combo.material.scss b/src/components/combo/themes/light/combo.material.scss index f556f5011..cd64dd80f 100644 --- a/src/components/combo/themes/light/combo.material.scss +++ b/src/components/combo/themes/light/combo.material.scss @@ -24,7 +24,7 @@ } [part~='case-icon'] { - padding: rem(8px); + padding: 0; } :host { diff --git a/src/components/input/themes/light/input.bootstrap.scss b/src/components/input/themes/light/input.bootstrap.scss index bff800e30..0f4a07f15 100644 --- a/src/components/input/themes/light/input.bootstrap.scss +++ b/src/components/input/themes/light/input.bootstrap.scss @@ -116,7 +116,8 @@ $input-background: var(--input-background, #fff) !default; [name='suffix']::slotted(*) { display: inline-flex; align-items: center; - width: fit-content; + min-width: fit-content; + box-sizing: content-box; height: 100%; padding-inline: pad-inline(8px, 12px, 16px); padding-block: pad-block(4px, 6px, 8px); diff --git a/src/components/input/themes/light/input.fluent.scss b/src/components/input/themes/light/input.fluent.scss index 536adcf62..3a018d356 100644 --- a/src/components/input/themes/light/input.fluent.scss +++ b/src/components/input/themes/light/input.fluent.scss @@ -33,7 +33,8 @@ $focused-height: calc(var(--size) - #{($focused-border-width * 2)}); [name='suffix']::slotted(*) { display: inline-flex; align-items: center; - width: fit-content; + min-width: fit-content; + box-sizing: content-box; height: 100%; padding-inline: pad-inline(8px, 12px, 16px); padding-block: pad-block(7px, 9px, 11px); diff --git a/src/components/input/themes/light/input.indigo.scss b/src/components/input/themes/light/input.indigo.scss index 0d8a3af1f..7900430e4 100644 --- a/src/components/input/themes/light/input.indigo.scss +++ b/src/components/input/themes/light/input.indigo.scss @@ -5,8 +5,8 @@ $label-focus-color: var(--label-focus-color, color(primary, 500)) !default; %suffix-preffix { display: inline-flex; align-items: center; - width: fit-content; - box-sizing: border-box; + min-width: fit-content; + box-sizing: content-box; height: calc(100% - #{rem(2px)}); padding-inline: pad-inline(8px, 12px, 16px); padding-block: pad-block(9px, 11px, 13px); diff --git a/src/components/input/themes/light/input.material.scss b/src/components/input/themes/light/input.material.scss index cb54c9e29..889b76342 100644 --- a/src/components/input/themes/light/input.material.scss +++ b/src/components/input/themes/light/input.material.scss @@ -33,8 +33,8 @@ $fs: rem(16px) !default; %suffix-preffix { display: inline-flex; align-items: center; - width: fit-content; - box-sizing: border-box; + min-width: fit-content; + box-sizing: content-box; height: 100%; padding-inline: pad-inline(8px, 12px, 16px); padding-block: pad-block(11px, 13px, 15px); From 065168bfd27241eb21778db31e914db8c07b9889 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 2 Dec 2022 15:14:51 +0200 Subject: [PATCH 63/82] fix(combo): requests update on firstUpdated --- src/components/combo/combo.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 6e6fe24e2..aaec6b3a7 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -370,11 +370,6 @@ export default class IgcComboComponent ); } - protected override async firstUpdated() { - await this.updateComplete; - this.requestUpdate(); - } - protected override async getUpdateComplete() { const result = await super.getUpdateComplete(); await this.toggleController.rendered; From ea609ae2b6d4b0703e8b45ebf52830fb6df07fc2 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 2 Dec 2022 23:36:44 +0200 Subject: [PATCH 64/82] spec(combo): add tests --- src/components/combo/combo.spec.ts | 608 ++++++++++++++++++++++++++--- 1 file changed, 556 insertions(+), 52 deletions(-) diff --git a/src/components/combo/combo.spec.ts b/src/components/combo/combo.spec.ts index 8eeb864e8..676598861 100644 --- a/src/components/combo/combo.spec.ts +++ b/src/components/combo/combo.spec.ts @@ -1,14 +1,16 @@ import { html } from 'lit'; -// import sinon from 'sinon'; +import sinon from 'sinon'; import { elementUpdated, expect, fixture } from '@open-wc/testing'; import { defineComponents } from '../common/definitions/defineComponents.js'; -// import IgcInputComponent from '../input/input'; +import IgcInputComponent from '../input/input'; import IgcComboComponent from './combo.js'; -// import IgcComboItemComponent from './combo-item.js'; +import IgcComboListComponent from './combo-list.js'; +import IgcComboItemComponent from './combo-item.js'; +import IgcListComponent from '../list/list.js'; // import IgcComboListComponent from './combo-list.js'; // import IgcComboHeaderComponent from './combo-header.js'; -describe('Combo Component', () => { +describe('Combo', () => { interface City { id: string; name: string; @@ -16,8 +18,8 @@ describe('Combo Component', () => { zip: string; } - // let input: IgcInputComponent; - // let searchInput: IgcInputComponent; + let input: IgcInputComponent; + let searchInput: IgcInputComponent; const cities: City[] = [ { @@ -56,35 +58,23 @@ describe('Combo Component', () => { country: 'United States', zip: '94103', }, - { - id: 'JP01', - name: 'Tokyo', - country: 'Japan', - zip: '163-8001', - }, - { - id: 'JP02', - name: 'Yokohama', - country: 'Japan', - zip: '781-0240', - }, - { - id: 'JP03', - name: 'Osaka', - country: 'Japan', - zip: '552-0021', - }, ]; let combo: IgcComboComponent; - // const comboItems = (el: IgcComboItemComponent) => - // [...el.querySelectorAll('igc-combo-item')] as IgcComboItemComponent[]; + let list: IgcComboListComponent; + let options: IgcComboListComponent; + const items = (combo: IgcComboComponent) => + [ + ...combo + .shadowRoot!.querySelector('igc-combo-list')! + .querySelectorAll('[part~="item"]'), + ] as IgcComboItemComponent[]; before(() => { defineComponents(IgcComboComponent); }); - describe('', () => { + describe('Component', () => { beforeEach(async () => { combo = await fixture>( html` { >` ); - // input = combo.shadowRoot!.querySelector( - // '[part="input"]' - // ) as IgcInputComponent; - // searchInput = combo.shadowRoot!.querySelector( - // '[part="search-input"]' - // ) as IgcInputComponent; + options = combo.shadowRoot!.querySelector( + '[part="list"]' + ) as IgcComboListComponent; + input = combo.shadowRoot!.querySelector( + '[part="input"]' + ) as IgcInputComponent; + searchInput = combo.shadowRoot!.querySelector( + '[part="search-input"]' + ) as IgcInputComponent; }); it('is accessible.', async () => { combo.open = true; - combo.label = 'Simple Select'; + combo.label = 'Simple Combo'; + await elementUpdated(combo); + await expect(combo).to.be.accessible({ + ignoredRules: ['aria-hidden-focus', 'nested-interactive'], + }); + }); + + it('is successfully created with default properties.', () => { + expect(document.querySelector('igc-combo')).to.exist; + expect(combo.data).to.equal(cities); + expect(combo.open).to.be.false; + expect(combo.name).to.be.undefined; + expect(combo.value).to.equal(''); + expect(combo.disabled).to.be.false; + expect(combo.required).to.be.false; + expect(combo.invalid).to.be.false; + expect(combo.autofocus).to.be.undefined; + expect(combo.autofocusList).to.be.false; + expect(combo.label).to.be.undefined; + expect(combo.placeholder).to.be.undefined; + expect(combo.placeholderSearch).to.equal('Search'); + expect(combo.outlined).to.be.false; + expect(combo.dir).to.equal('auto'); + expect(combo.flip).to.be.true; + expect(combo.sameWidth).to.be.true; + expect(combo.valueKey).to.equal('id'); + expect(combo.displayKey).to.equal('name'); + expect(combo.groupKey).to.equal('country'); + expect(combo.groupSorting).to.equal('asc'); + expect(combo.filteringOptions).includes({ + filterKey: combo.displayKey, + caseSensitive: false, + }); + expect(combo.caseSensitiveIcon).to.be.false; + expect(combo.disableFiltering).to.be.false; + }); + + it('correctly applies input related properties to encapsulated inputs', async () => { + combo.label = 'Select Label'; + combo.placeholder = 'Select Placeholder'; + combo.placeholderSearch = 'Select Placeholder'; + await elementUpdated(combo); + + expect(input.value).to.equal(combo.value); + expect(input.placeholder).to.equal(combo.placeholder); + expect(input.label).to.equal(combo.label); + expect(input.disabled).to.equal(combo.disabled); + expect(input.required).to.equal(input.required); + expect(input.invalid).to.equal(combo.invalid); + expect(input.outlined).to.equal(combo.outlined); + expect(input.dir).to.equal(combo.dir); + expect(input.autofocus).to.equal(combo.autofocus); + + expect(searchInput.placeholder).to.equal(combo.placeholderSearch); + expect(searchInput.dir).to.equal(combo.dir); + }); + + it('should open the menu upon calling the show method', async () => { + combo.show(); + await elementUpdated(combo); + + expect(combo.open).to.be.true; + }); + + it('should close the menu upon calling the hide method', async () => { + combo.hide(); + await elementUpdated(combo); + + expect(combo.open).to.be.false; + }); + + it('should toggle the menu upon calling the toggle method', async () => { + combo.toggle(); + await elementUpdated(combo); + + expect(combo.open).to.be.true; + + combo.toggle(); + await elementUpdated(combo); + + expect(combo.open).to.be.false; + }); + + it('should open the menu upon clicking on the input', async () => { + const eventSpy = sinon.spy(combo, 'emitEvent'); + input.click(); + + await elementUpdated(combo); + + expect(eventSpy).calledWith('igcOpening'); + expect(eventSpy).calledWith('igcOpened'); + expect(combo.open).to.be.true; + }); + + it('should hide the menu upon clicking on the input', async () => { + const eventSpy = sinon.spy(combo, 'emitEvent'); + combo.show(); + input.click(); + + await elementUpdated(combo); + + expect(eventSpy).calledWith('igcClosing'); + expect(eventSpy).calledWith('igcClosed'); + expect(combo.open).to.be.false; + }); + + it('should be able to cancel the igcOpening event', async () => { + combo.open = false; + combo.addEventListener('igcOpening', (event: CustomEvent) => { + event.preventDefault(); + }); + const eventSpy = sinon.spy(combo, 'emitEvent'); + input.click(); + await elementUpdated(combo); + + expect(eventSpy).calledOnceWithExactly('igcOpening', { + cancelable: true, + }); + expect(eventSpy).not.calledWith('igcOpened'); + }); + + it('should be able to cancel the igcClosing event', async () => { + combo.open = true; + combo.addEventListener('igcClosing', (event: CustomEvent) => { + event.preventDefault(); + }); + const eventSpy = sinon.spy(combo, 'emitEvent'); + input.click(); + await elementUpdated(combo); + + expect(eventSpy).calledOnceWithExactly('igcClosing', { + cancelable: true, + }); + expect(eventSpy).not.calledWith('igcClosed'); + }); + + it('should focus the input when the host is focused', async () => { + combo.focus(); + await elementUpdated(combo); + + expect(document.activeElement).to.equal(combo); + }); + + it('should blur the input when the host is blurred', async () => { + combo.blur(); + await elementUpdated(combo); + + expect(document.activeElement).not.to.equal(combo); + }); + + it('should render all data items as combo-list items', async () => { + combo.open = true; + + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + const cityNames: string[] = []; + + items(combo).forEach((item) => { + cityNames.push(item.textContent!); + }); + + cities.forEach((city) => { + expect(cityNames).to.include(city[combo.displayKey!]); + }); + }); + + it('should configure the filtering options by attribute', async () => { + combo.setAttribute( + 'filtering-options', + '{"filterKey": "zip", "caseSensitive": true}' + ); + await elementUpdated(combo); + + expect(combo.filteringOptions.filterKey).to.equal('zip'); + expect(combo.filteringOptions.caseSensitive).to.equal(true); + }); + + it('should select/deselect an item by value key', async () => { + const item = cities[0]; + combo.select([item[combo.valueKey!]]); + + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + + const selected = items(combo).find((item) => item.selected); + expect(selected?.textContent).to.equal(item[combo.displayKey!]); + + combo.deselect([item[combo.valueKey!]]); + + await elementUpdated(combo); + await elementUpdated(list); + + items(combo).forEach((item) => { + expect(item.selected).to.be.false; + }); + }); + + it('should select/deselect an item by value when no valueKey is present', async () => { + combo.valueKey = undefined; + await elementUpdated(combo); + + const item = cities[0]; + combo.select([item]); + + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + + const selected = items(combo).find((item) => item.selected); + expect(selected?.textContent).to.equal(item[combo.displayKey!]); + + combo.deselect([item]); + + await elementUpdated(combo); + await elementUpdated(list); + + items(combo).forEach((item) => { + expect(item.selected).to.be.false; + }); + }); + + it('should select/deselect all items', async () => { + combo.select(); + await elementUpdated(combo); + await elementUpdated(list); + + items(combo).forEach((item) => { + expect(item.selected).to.be.true; + }); + + combo.deselect(); + await elementUpdated(combo); + await elementUpdated(list); + + items(combo).forEach((item) => { + expect(item.selected).to.be.false; + }); + }); + + it('should clear the selection by pressing on the clear button', async () => { + combo.select(); + + await elementUpdated(combo); + await elementUpdated(list); + + const button = combo.shadowRoot!.querySelector('[part="clear-icon"]'); + + (button! as HTMLSpanElement).click(); + + await elementUpdated(combo); + await elementUpdated(list); + + items(combo).forEach((item) => { + expect(item.selected).to.be.false; + }); + }); + + it('should toggle case sensitivity by pressing on the case sensitive icon', async () => { + const button = combo.shadowRoot!.querySelector( + '[part~="case-icon"]' + ) as HTMLElement; + expect(combo.filteringOptions.caseSensitive).to.be.false; + + button.click(); + await elementUpdated(combo); + + expect(combo.filteringOptions.caseSensitive).to.be.true; + + button.click(); + await elementUpdated(combo); + expect(combo.filteringOptions.caseSensitive).to.be.false; + }); + + it('should not fire igcChange event on selection/deselection via methods calls', async () => { + const item = cities[0]; + combo.select([item[combo.valueKey!]]); + + combo.addEventListener('igcChange', (event: CustomEvent) => + event.preventDefault() + ); + + const eventSpy = sinon.spy(combo, 'emitEvent'); + expect(eventSpy).not.calledWith('igcChange'); + }); + + it('should fire igcChange event on selection/deselection on mouse click', async () => { + const eventSpy = sinon.spy(combo, 'emitEvent'); + combo.open = true; + await elementUpdated(combo); - await expect(combo).to.be.accessible(); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + + items(combo)[0].click(); + expect(eventSpy).calledWith('igcChange'); + }); + + it('reports validity when required', async () => { + const validity = sinon.spy(combo, 'reportValidity'); + + combo.required = true; + await elementUpdated(combo); + + combo.checkValidity(); + expect(validity).to.have.returned(false); + expect(combo.invalid).to.be.true; + + combo.select(); + await elementUpdated(combo); + combo.checkValidity(); + + expect(validity).to.have.returned(true); + expect(combo.invalid).to.be.false; + }); + + it('reports validity when not required', async () => { + const validity = sinon.spy(combo, 'reportValidity'); + + combo.required = false; + await elementUpdated(combo); + + combo.checkValidity(); + expect(validity).to.have.returned(true); + expect(combo.invalid).to.be.false; + + combo.deselect(); + await elementUpdated(combo); + combo.checkValidity(); + + expect(validity).to.have.returned(true); + expect(combo.invalid).to.be.false; + }); + + it('opens the list of options when Down or Alt+Down keys are pressed', async () => { + combo.open = false; + pressKey(input, 'ArrowDown', 1, { altKey: false }); + expect(combo.open).to.be.true; + + combo.open = false; + pressKey(input, 'ArrowDown', 1, { altKey: true }); + expect(combo.open).to.be.true; + }); + + it('closes the list of options when search input is on focus and the Up key is pressed', async () => { + combo.show(); + await elementUpdated(combo); + expect(combo.open).to.be.true; + + pressKey(searchInput, 'ArrowUp', 1, { altKey: false }); + expect(combo.open).to.be.false; + }); + + it('activates the first list item when clicking pressing ArrowDown when the search input is on focus', async () => { + combo.show(); + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + expect(items(combo)[0].active).to.be.false; + pressKey(searchInput, 'ArrowDown', 1, { altKey: false }); + + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + expect(items(combo)[0].active).to.be.true; + }); + + it('navigates between the items with ArrowUp and ArrowDown keys', async () => { + combo.autofocusList = true; + await elementUpdated(combo); + + combo.show(); + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + const options = combo.shadowRoot!.querySelector( + '[part="list"]' + ) as IgcListComponent; + expect(items(combo)[0].active).to.be.false; + pressKey(options, 'ArrowDown', 2, { altKey: false }); + + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + expect(items(combo)[1].active).to.be.true; + pressKey(options, 'ArrowUp', 1, { altKey: false }); + + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + expect(items(combo)[0].active).to.be.true; + }); + + it('should activate the first list item by pressing the Home key', async () => { + combo.autofocusList = true; + await elementUpdated(combo); + + combo.show(); + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + pressKey(options, 'Home', 1, { altKey: false }); + + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + expect(items(combo)[0].active).to.be.true; + }); + + it('should activate the last list item by pressing the End key', async () => { + combo.autofocusList = true; + await elementUpdated(combo); + + combo.show(); + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + pressKey(options, 'End', 1, { altKey: false }); + + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + const itms = items(combo); + expect(itms[itms.length - 1].active).to.be.true; + }); + + it('should select the active item by pressing the Space key', async () => { + combo.autofocusList = true; + await elementUpdated(combo); + + combo.show(); + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + pressKey(options, 'ArrowDown', 2, { altKey: false }); + pressKey(options, ' ', 1, { altKey: false }); + + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + const itms = items(combo); + expect(itms[1].active).to.be.true; + expect(itms[1].selected).to.be.true; + expect(combo.open).to.be.true; + }); + + it('should select the active item and close the menu by pressing the Enter key', async () => { + combo.autofocusList = true; + await elementUpdated(combo); + + combo.show(); + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + pressKey(options, 'ArrowDown', 2, { altKey: false }); + pressKey(options, 'Enter', 1, { altKey: false }); + + await elementUpdated(combo); + await elementUpdated(list); + await new Promise((resolve) => { + setTimeout(resolve, 200); + }); + + const itms = items(combo); + expect(itms[1].active).to.be.true; + expect(itms[1].selected).to.be.true; + expect(combo.open).to.be.false; }); }); }); -// const pressKey = ( -// target: HTMLElement, -// key: string, -// times = 1, -// options?: Object -// ) => { -// for (let i = 0; i < times; i++) { -// target.dispatchEvent( -// new KeyboardEvent('keydown', { -// key: key, -// bubbles: true, -// composed: true, -// ...options, -// }) -// ); -// } -// }; + +const pressKey = ( + target: HTMLElement, + key: string, + times = 1, + options?: Object +) => { + for (let i = 0; i < times; i++) { + target.dispatchEvent( + new KeyboardEvent('keydown', { + key: key, + bubbles: true, + composed: true, + ...options, + }) + ); + } +}; From 4dc04c4aaf1bb5c9cfc0f523f628cf280fc415a2 Mon Sep 17 00:00:00 2001 From: Marin Popov Date: Mon, 5 Dec 2022 11:58:37 +0200 Subject: [PATCH 65/82] style(input, icon, combo): fix styling issues (#574) * style(input, icon, combo) fix styling issues * refactor(combo): sizing issue Co-authored-by: Simeon Simeonoff --- src/components/combo/combo.spec.ts | 1 - src/components/combo/combo.ts | 4 ---- src/components/combo/themes/light/combo.base.scss | 6 +++--- src/components/combo/themes/light/combo.bootstrap.scss | 8 ++++---- src/components/combo/themes/light/combo.indigo.scss | 4 ---- src/components/combo/themes/light/combo.material.scss | 4 ---- src/components/icon/icon.material.scss | 4 ++-- src/components/input/themes/light/input.bootstrap.scss | 4 +--- src/components/input/themes/light/input.fluent.scss | 4 +--- src/components/input/themes/light/input.indigo.scss | 6 ++---- src/components/input/themes/light/input.material.scss | 4 +--- stories/combo.stories.ts | 2 +- 12 files changed, 15 insertions(+), 36 deletions(-) diff --git a/src/components/combo/combo.spec.ts b/src/components/combo/combo.spec.ts index 676598861..abe5114f6 100644 --- a/src/components/combo/combo.spec.ts +++ b/src/components/combo/combo.spec.ts @@ -122,7 +122,6 @@ describe('Combo', () => { expect(combo.outlined).to.be.false; expect(combo.dir).to.equal('auto'); expect(combo.flip).to.be.true; - expect(combo.sameWidth).to.be.true; expect(combo.valueKey).to.equal('id'); expect(combo.displayKey).to.equal('name'); expect(combo.groupKey).to.equal('country'); diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index aaec6b3a7..535237fd4 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -213,10 +213,6 @@ export default class IgcComboComponent @property({ type: Boolean }) public flip = true; - /** @hidden @internal */ - @property({ type: Boolean, attribute: 'same-width' }) - public sameWidth = true; - /** * The key in the data source used when selecting items. * @attr value-key diff --git a/src/components/combo/themes/light/combo.base.scss b/src/components/combo/themes/light/combo.base.scss index 85eddfcc6..4960664af 100644 --- a/src/components/combo/themes/light/combo.base.scss +++ b/src/components/combo/themes/light/combo.base.scss @@ -39,6 +39,7 @@ @include border-radius(rem(4px)); position: absolute; + width: 100%; background: color(surface); color: color(gray, 700); box-shadow: elevation(8); @@ -54,7 +55,7 @@ igc-input::part(container) { background: transparent; border-inline-start: 0; - border-inline-end: 0; + border-inline-end-color: transparent; border-block-start: 0; border-radius: 0; } @@ -67,7 +68,7 @@ border-radius: 0; padding: 0; border-inline-start: 0; - border-inline-end: 0; + border-inline-end-color: transparent; border-block-start: 0; box-shadow: none; @@ -94,7 +95,6 @@ [part='case-icon'] { color: color(gray, 600); - min-width: rem(20px); } [part='case-icon active'] { diff --git a/src/components/combo/themes/light/combo.bootstrap.scss b/src/components/combo/themes/light/combo.bootstrap.scss index 600a810e8..d23a70d61 100644 --- a/src/components/combo/themes/light/combo.bootstrap.scss +++ b/src/components/combo/themes/light/combo.bootstrap.scss @@ -2,7 +2,7 @@ @use '../../../select/themes/light/select.bootstrap'; [part='clear-icon'] { - border-inline-end: 1px solid color(gray, 400); + border-inline-end: rem(1px) solid color(gray, 400); } [part='list-wrapper'] { @@ -14,14 +14,14 @@ [part='search-input'] { [part~='case-icon'] { - border-block-end: 1px solid color(gray, 400); + border-block-end: rem(1px) solid color(gray, 400); transition: border .15s ease-out; } &:focus, &:focus-within { [part~='case-icon'] { - border-block-end: 1px solid color(primary, 500); + border-block-end: rem(1px) solid color(primary, 500); } } } @@ -29,6 +29,6 @@ :host([disabled]) { [part='clear-icon'] { - border-inline-end: 1px solid color(gray, 300); + border-inline-end: rem(1px) solid color(gray, 300); } } diff --git a/src/components/combo/themes/light/combo.indigo.scss b/src/components/combo/themes/light/combo.indigo.scss index b478044b5..c40a7d0d8 100644 --- a/src/components/combo/themes/light/combo.indigo.scss +++ b/src/components/combo/themes/light/combo.indigo.scss @@ -1,10 +1,6 @@ @use '../../../../styles/utilities' as *; @use '../../../select/themes/light/select.indigo'; -[part~='case-icon'] { - padding: 0; -} - :host { ::slotted([slot='helper-text']) { @include type-style('caption'); diff --git a/src/components/combo/themes/light/combo.material.scss b/src/components/combo/themes/light/combo.material.scss index cd64dd80f..7ea041b3c 100644 --- a/src/components/combo/themes/light/combo.material.scss +++ b/src/components/combo/themes/light/combo.material.scss @@ -23,10 +23,6 @@ } } -[part~='case-icon'] { - padding: 0; -} - :host { ::slotted([slot='helper-text']) { @include type-style('caption'); diff --git a/src/components/icon/icon.material.scss b/src/components/icon/icon.material.scss index 7ae361dad..db6e3e216 100644 --- a/src/components/icon/icon.material.scss +++ b/src/components/icon/icon.material.scss @@ -2,8 +2,8 @@ @use '../../styles/utilities' as *; svg { - height: 100%; - width: 100%; + height: var(--size); + width: var(--size); } :host { diff --git a/src/components/input/themes/light/input.bootstrap.scss b/src/components/input/themes/light/input.bootstrap.scss index 0f4a07f15..4d29b9ce6 100644 --- a/src/components/input/themes/light/input.bootstrap.scss +++ b/src/components/input/themes/light/input.bootstrap.scss @@ -116,11 +116,9 @@ $input-background: var(--input-background, #fff) !default; [name='suffix']::slotted(*) { display: inline-flex; align-items: center; - min-width: fit-content; - box-sizing: content-box; + width: max-content; height: 100%; padding-inline: pad-inline(8px, 12px, 16px); - padding-block: pad-block(4px, 6px, 8px); } [part='suffix'] { diff --git a/src/components/input/themes/light/input.fluent.scss b/src/components/input/themes/light/input.fluent.scss index 3a018d356..deed7ae77 100644 --- a/src/components/input/themes/light/input.fluent.scss +++ b/src/components/input/themes/light/input.fluent.scss @@ -33,11 +33,9 @@ $focused-height: calc(var(--size) - #{($focused-border-width * 2)}); [name='suffix']::slotted(*) { display: inline-flex; align-items: center; - min-width: fit-content; - box-sizing: content-box; + width: max-content; height: 100%; padding-inline: pad-inline(8px, 12px, 16px); - padding-block: pad-block(7px, 9px, 11px); } [part='prefix'] { diff --git a/src/components/input/themes/light/input.indigo.scss b/src/components/input/themes/light/input.indigo.scss index 7900430e4..f8535a740 100644 --- a/src/components/input/themes/light/input.indigo.scss +++ b/src/components/input/themes/light/input.indigo.scss @@ -5,11 +5,9 @@ $label-focus-color: var(--label-focus-color, color(primary, 500)) !default; %suffix-preffix { display: inline-flex; align-items: center; - min-width: fit-content; - box-sizing: content-box; - height: calc(100% - #{rem(2px)}); + width: max-content; + height: 100%; padding-inline: pad-inline(8px, 12px, 16px); - padding-block: pad-block(9px, 11px, 13px); } :host { diff --git a/src/components/input/themes/light/input.material.scss b/src/components/input/themes/light/input.material.scss index 889b76342..eaf89d86c 100644 --- a/src/components/input/themes/light/input.material.scss +++ b/src/components/input/themes/light/input.material.scss @@ -33,11 +33,9 @@ $fs: rem(16px) !default; %suffix-preffix { display: inline-flex; align-items: center; - min-width: fit-content; - box-sizing: content-box; + width: max-content; height: 100%; padding-inline: pad-inline(8px, 12px, 16px); - padding-block: pad-block(11px, 13px, 15px); } :host { diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index e2d7495be..7c9047484 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -90,7 +90,7 @@ const metadata = { sameWidth: { type: 'boolean', control: 'boolean', - defaultValue: true, + defaultValue: false, }, groupSorting: { type: '"asc" | "desc"', From 6a1720f97e1451e0d2fcdc642d254db4ab9b700c Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 5 Dec 2022 12:14:35 +0200 Subject: [PATCH 66/82] fix(combo): group sorting --- src/components/combo/operations/group.ts | 6 ++---- stories/combo.stories.ts | 6 ------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/combo/operations/group.ts b/src/components/combo/operations/group.ts index 0458fb826..ffd189f5b 100644 --- a/src/components/combo/operations/group.ts +++ b/src/components/combo/operations/group.ts @@ -64,10 +64,8 @@ export default class GroupDataOperation { groups.forEach((group) => { group.sort((a: ComboRecord, b: ComboRecord) => { - if (!a.header && !b.header) { - return this.compareObjects(a, b, displayKey!, direction); - } - return 1; + if (a.header || b.header) return; + return this.compareObjects(a, b, displayKey!, direction); }); }); diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 7c9047484..7d0a244c1 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -87,11 +87,6 @@ const metadata = { control: 'boolean', defaultValue: true, }, - sameWidth: { - type: 'boolean', - control: 'boolean', - defaultValue: false, - }, groupSorting: { type: '"asc" | "desc"', description: @@ -138,7 +133,6 @@ interface ArgTypes { dir: 'ltr' | 'rtl' | 'auto'; open: boolean; flip: boolean; - sameWidth: boolean; groupSorting: 'asc' | 'desc'; caseSensitiveIcon: boolean; disableFiltering: boolean; From 652ef70366fba0eb2d31ea29db8123d8bc252ca4 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 5 Dec 2022 13:19:23 +0200 Subject: [PATCH 67/82] refactor(combo): split up the template --- src/components/combo/combo.ts | 236 +++++++++++++++++++--------------- 1 file changed, 132 insertions(+), 104 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 535237fd4..2cf143942 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -568,119 +568,147 @@ export default class IgcComboComponent return this.inputSuffix.length > 0; } - protected override render() { + private renderToggleIcon() { + return html` + + + + + + `; + } + + private renderClearIcon() { const { selected } = this.selectionController; - return html` + return html` + + + + `; + } + + private renderInput() { + return html` { + e.preventDefault(); + this._toggle(true); + }} + value=${ifDefined(this.value)} + placeholder=${ifDefined(this.placeholder)} + label=${ifDefined(this.label)} + dir=${this.dir} + @igcFocus=${(e: Event) => e.stopPropagation()} + @igcBlur=${(e: Event) => e.stopPropagation()} + @keydown=${this.navigationController.navigateHost.bind( + this.navigationController + )} + .disabled="${this.disabled}" + .required=${this.required} + .invalid=${this.invalid} + .outlined=${this.outlined} + .autofocus=${this.autofocus} + readonly + > + + + + ${this.renderClearIcon()} + + + + ${this.renderToggleIcon()} + `; + } + + private renderSearchInput() { + return html`
{ - e.preventDefault(); - this._toggle(true); - }} - value=${ifDefined(this.value)} - placeholder=${ifDefined(this.placeholder)} - label=${ifDefined(this.label)} - dir=${this.dir} @igcFocus=${(e: Event) => e.stopPropagation()} @igcBlur=${(e: Event) => e.stopPropagation()} - @keydown=${this.navigationController.navigateHost.bind( - this.navigationController - )} - .disabled="${this.disabled}" - .required=${this.required} - .invalid=${this.invalid} - .outlined=${this.outlined} - .autofocus=${this.autofocus} - readonly + @igcInput=${this.handleSearchInput} + @keydown=${(e: KeyboardEvent) => + this.navigationController.navigateInput(e, this.list)} + dir=${this.dir} > - - - - - - - - - - - - - - - - + -
`; + } + + private renderEmptyTemplate() { + return html` 0}> +
The list is empty
+
`; + } + + private renderList() { + return html`
+ ${this.renderSearchInput()} + + -
- e.stopPropagation()} - @igcBlur=${(e: Event) => e.stopPropagation()} - @igcInput=${this.handleSearchInput} - @keydown=${(e: KeyboardEvent) => - this.navigationController.navigateInput(e, this.list)} - dir=${this.dir} - > - - -
- - - - 0}> -
The list is empty
-
- -
-
- -
+ + + ${this.renderEmptyTemplate()} +
`; + } + + private renderHelperText() { + return html`
+ +
`; + } + + protected override render() { + return html` + ${this.renderInput()}${this.renderList()}${this.renderHelperText()} `; } } From 508c7008e30574ab83e857d8dfc3c347afac8966 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 5 Dec 2022 14:46:40 +0200 Subject: [PATCH 68/82] refactor(combo): change icons based on theme --- src/components/combo/combo.ts | 27 ++++++++++++++++--- .../combo/themes/light/combo.material.scss | 2 +- src/components/icon/icon.registry.ts | 3 +++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 2cf143942..5016996cb 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -36,6 +36,11 @@ import { partNameMap } from '../common/util.js'; import { filteringOptionsConverter } from './utils/converters.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import { Constructor } from '../common/mixins/constructor.js'; +import type { + ReactiveTheme, + ThemeController, + Theme, +} from '../../theming/types.js'; import { blazorAdditionalDependencies } from '../common/decorators/blazorAdditionalDependencies.js'; defineComponents( @@ -92,7 +97,7 @@ export default class IgcComboComponent extends EventEmitterMixin>( LitElement ) - implements Partial + implements Partial, ReactiveTheme { public static readonly tagName = 'igc-combo'; public static styles = styles; @@ -101,6 +106,8 @@ export default class IgcComboComponent protected selectionController = new SelectionController(this); protected dataController = new DataController(this); protected toggleController!: IgcToggleController; + protected themeController!: ThemeController; + protected theme!: Theme; @queryAssignedElements({ slot: 'helper-text' }) protected helperText!: Array; @@ -366,6 +373,14 @@ export default class IgcComboComponent ); } + public themeAdopted(controller: ThemeController) { + this.themeController = controller; + } + + protected override willUpdate() { + this.theme = this.themeController.theme; + } + protected override async getUpdateComplete() { const result = await super.getUpdateComplete(); await this.toggleController.rendered; @@ -569,11 +584,16 @@ export default class IgcComboComponent } private renderToggleIcon() { + const openIcon = + this.theme === 'material' ? 'keyboard_arrow_up' : 'arrow_drop_up'; + const closeIcon = + this.theme === 'material' ? 'keyboard_arrow_down' : 'arrow_drop_down'; + return html` @@ -584,6 +604,7 @@ export default class IgcComboComponent private renderClearIcon() { const { selected } = this.selectionController; + const icon = this.theme === 'material' ? 'chip_cancel' : 'clear'; return html` > diff --git a/src/components/combo/themes/light/combo.material.scss b/src/components/combo/themes/light/combo.material.scss index 7ea041b3c..1e5816f88 100644 --- a/src/components/combo/themes/light/combo.material.scss +++ b/src/components/combo/themes/light/combo.material.scss @@ -5,7 +5,7 @@ background: color(gray, 300); } -[part='target']:not([outlined]) { +[part='input']:not([outlined]) { &:focus-within { [part='toggle-icon'] { background: color(gray, 400, .3); diff --git a/src/components/icon/icon.registry.ts b/src/components/icon/icon.registry.ts index 245df1a44..36fdcd4d7 100644 --- a/src/components/icon/icon.registry.ts +++ b/src/components/icon/icon.registry.ts @@ -102,4 +102,7 @@ const internalIcons: IconCollection = { star: ``, star_border: ``, case_sensitive: ``, + clear: ``, + arrow_drop_up: ``, + arrow_drop_down: ``, }; From 0009814768c37d1465af16c157fb86e4a45b460d Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 5 Dec 2022 14:48:56 +0200 Subject: [PATCH 69/82] fix(select): toggle icon inconsistent with combo --- src/components/select/select.ts | 37 +++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/components/select/select.ts b/src/components/select/select.ts index 8fe6a4668..14fe6b3a3 100644 --- a/src/components/select/select.ts +++ b/src/components/select/select.ts @@ -1,4 +1,4 @@ -import { html } from 'lit'; +import { html, PropertyValueMap } from 'lit'; import { property, query, @@ -22,6 +22,11 @@ import IgcInputComponent from '../input/input.js'; import IgcSelectGroupComponent from './select-group.js'; import IgcSelectHeaderComponent from './select-header.js'; import IgcSelectItemComponent from './select-item.js'; +import type { + ReactiveTheme, + ThemeController, + Theme, +} from '../../theming/types.js'; import { styles } from './themes/light/select.base.css.js'; import { styles as bootstrap } from './themes/light/select.bootstrap.css.js'; import { styles as fluent } from './themes/light/select.fluent.css.js'; @@ -72,15 +77,20 @@ export interface IgcSelectEventMap extends IgcDropdownEventMap { * @csspart toggle-icon - The toggle icon wrapper. * @csspart helper-text - The helper text wrapper. */ -export default class IgcSelectComponent extends EventEmitterMixin< - IgcSelectEventMap, - Constructor ->(IgcDropdownComponent) { +export default class IgcSelectComponent + extends EventEmitterMixin< + IgcSelectEventMap, + Constructor + >(IgcDropdownComponent) + implements ReactiveTheme +{ /** @private */ public static readonly tagName = 'igc-select'; public static styles = styles; private searchTerm = ''; private lastKeyTime = Date.now(); + protected themeController!: ThemeController; + protected theme!: Theme; private readonly targetKeyHandlers: Map = new Map( Object.entries({ @@ -205,6 +215,16 @@ export default class IgcSelectComponent extends EventEmitterMixin< }); } + public themeAdopted(controller: ThemeController) { + this.themeController = controller; + } + + protected override willUpdate(changes: PropertyValueMap) { + super.willUpdate(changes); + + this.theme = this.themeController.theme; + } + /** Override the dropdown target focusout behavior to prevent the focus from * being returned to the target element when the select loses focus. */ protected override handleFocusout() {} @@ -407,6 +427,11 @@ export default class IgcSelectComponent extends EventEmitterMixin< } protected override render() { + const openIcon = + this.theme === 'material' ? 'keyboard_arrow_up' : 'arrow_drop_up'; + const closeIcon = + this.theme === 'material' ? 'keyboard_arrow_down' : 'arrow_drop_down'; + return html`
From 7ec1d841880fe3f1cea5a3cf8d44adb4dc3bafd9 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 5 Dec 2022 15:46:18 +0200 Subject: [PATCH 70/82] refactor(combo): change exported parts --- src/components/combo/combo.spec.ts | 2 +- src/components/combo/combo.ts | 8 ++++---- src/components/combo/themes/light/combo.material.scss | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/combo/combo.spec.ts b/src/components/combo/combo.spec.ts index abe5114f6..94f156da7 100644 --- a/src/components/combo/combo.spec.ts +++ b/src/components/combo/combo.spec.ts @@ -89,7 +89,7 @@ describe('Combo', () => { '[part="list"]' ) as IgcComboListComponent; input = combo.shadowRoot!.querySelector( - '[part="input"]' + 'igc-input#target' ) as IgcInputComponent; searchInput = combo.shadowRoot!.querySelector( '[part="search-input"]' diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 5016996cb..5cbc66f27 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -72,6 +72,7 @@ defineComponents( * * @csspart label - The encapsulated text label. * @csspart input - The main input field. + * @csspart native-input - The native input of the main input field. * @csspart prefix - The prefix wrapper. * @csspart suffix - The suffix wrapper. * @csspart toggle-icon - The toggle icon wrapper. @@ -122,7 +123,7 @@ export default class IgcComboComponent @query('[part="search-input"]') public input!: IgcInputComponent; - @query('[part="input"]') + @query('igc-input#target') private target!: IgcInputComponent; @query('igc-combo-list') @@ -624,8 +625,7 @@ export default class IgcComboComponent private renderInput() { return html` e.stopPropagation()} @igcBlur=${(e: Event) => e.stopPropagation()} @igcInput=${this.handleSearchInput} diff --git a/src/components/combo/themes/light/combo.material.scss b/src/components/combo/themes/light/combo.material.scss index 1e5816f88..ab9027d70 100644 --- a/src/components/combo/themes/light/combo.material.scss +++ b/src/components/combo/themes/light/combo.material.scss @@ -5,7 +5,7 @@ background: color(gray, 300); } -[part='input']:not([outlined]) { +igc-input#target:not([outlined]) { &:focus-within { [part='toggle-icon'] { background: color(gray, 400, .3); From 4b4d53f073d41baa0f2316f90dc189e9fb976335 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 5 Dec 2022 16:15:46 +0200 Subject: [PATCH 71/82] docs(combo): update parts list --- src/components/combo/combo.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 5cbc66f27..a533ba0d1 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -77,6 +77,7 @@ defineComponents( * @csspart suffix - The suffix wrapper. * @csspart toggle-icon - The toggle icon wrapper. * @csspart clear-icon - The clear icon wrapper. + * @csspart case-icon - The case icon wrapper. * @csspart helper-text - The helper text wrapper. * @csspart search-input - The search input field. * @csspart list-wrapper - The list of options wrapper. From 3b182460614f8b97d4b55a095097e74ccc0ac1c1 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 5 Dec 2022 19:00:14 +0200 Subject: [PATCH 72/82] refactor(combo): mark props/methods as protected --- src/components/combo/combo.ts | 15 ++---- .../combo/controllers/navigation.ts | 47 ++++++++++++++----- src/components/combo/controllers/selection.ts | 15 ++++-- stories/combo.stories.ts | 6 +-- 4 files changed, 53 insertions(+), 30 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index a533ba0d1..adb4d64b7 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -120,9 +120,8 @@ export default class IgcComboComponent @queryAssignedElements({ slot: 'prefix' }) protected inputPrefix!: Array; - /** @hidden @internal */ @query('[part="search-input"]') - public input!: IgcInputComponent; + protected input!: IgcInputComponent; @query('igc-input#target') private target!: IgcInputComponent; @@ -308,9 +307,8 @@ export default class IgcComboComponent return html`${this.groupKey && item[this.groupKey]}`; }; - /** @hidden @internal */ @state() - public dataState: Array> = []; + protected dataState: Array> = []; @watch('data') protected dataChanged() { @@ -457,8 +455,7 @@ export default class IgcComboComponent return this.emitEvent('igcClosing', args); } - /** @hidden @internal */ - public async _show(emit = true) { + protected async _show(emit = true) { if (this.open) return; if (emit && !this.handleOpening()) return; this.open = true; @@ -478,8 +475,7 @@ export default class IgcComboComponent this._show(false); } - /** @hidden @internal */ - public async _hide(emit = true) { + protected async _hide(emit = true) { if (!this.open) return; if (emit && !this.handleClosing()) return; this.open = false; @@ -556,8 +552,7 @@ export default class IgcComboComponent this.input.focus(); } - /** @internal @hidden */ - public toggleSelect(index: number) { + protected toggleSelect(index: number) { this.selectionController.changeSelection(index); this.navigationController.active = index; } diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts index 9ffa06e90..e6ca3419c 100644 --- a/src/components/combo/controllers/navigation.ts +++ b/src/components/combo/controllers/navigation.ts @@ -43,19 +43,42 @@ export class NavigationController protected _active = START_INDEX; + public get input() { + // @ts-expect-error protected access + return this.host.input; + } + + public get dataState() { + // @ts-expect-error protected access + return this.host.dataState; + } + + public show() { + // @ts-expect-error protected access + this.host._show(true); + } + + public hide() { + // @ts-expect-error protected access + this.host._hide(true); + } + + public toggleSelect(index: number) { + // @ts-expect-error protected access + this.host.toggleSelect(index); + } + protected get currentItem() { const item = this.active; return item === START_INDEX ? START_INDEX : item; } protected get firstItem() { - return this.host.dataState.findIndex( - (i: ComboRecord) => i.header !== true - ); + return this.dataState.findIndex((i: ComboRecord) => i.header !== true); } protected get lastItem() { - return this.host.dataState.length - 1; + return this.dataState.length - 1; } public get active() { @@ -82,20 +105,20 @@ export class NavigationController } protected space() { - const item = this.host.dataState[this.active]; + const item = this.dataState[this.active]; if (!item.header) { - this.host.toggleSelect(this.active); + this.toggleSelect(this.active); } } protected escape() { - this.host._hide(); + this.hide(); } protected enter() { this.space(); - this.host._hide(); + this.hide(); } protected inputArrowDown(container: IgcComboListComponent) { @@ -107,7 +130,7 @@ export class NavigationController } protected hostArrowDown() { - this.host._show(); + this.show(); } protected arrowDown(container: IgcComboListComponent) { @@ -125,7 +148,7 @@ export class NavigationController if (next === -1) { if (this.active === this.lastItem) return; - this.host.input.focus(); + this.input.focus(); return; } @@ -134,7 +157,7 @@ export class NavigationController protected getNearestItem(startIndex: number, direction: number) { let index = startIndex; - const items = this.host.dataState; + const items = this.dataState; if (items[index + direction]?.header) { this.getNearestItem((index += direction), direction); @@ -156,7 +179,7 @@ export class NavigationController } public navigateTo(item: T, container: IgcComboListComponent) { - this.active = this.host.dataState.findIndex((i) => i === item); + this.active = this.dataState.findIndex((i) => i === item); container.scrollToIndex(this.active); } diff --git a/src/components/combo/controllers/selection.ts b/src/components/combo/controllers/selection.ts index 7ae7f0f00..414b9efef 100644 --- a/src/components/combo/controllers/selection.ts +++ b/src/components/combo/controllers/selection.ts @@ -11,6 +11,11 @@ export class SelectionController { private _selected: Set = new Set(); + public get dataState() { + // @ts-expect-error protected access + return this.host.dataState; + } + public getValue(items: T[]) { return items .map((value) => { @@ -32,7 +37,7 @@ export class SelectionController private getItemsByValueKey(keys: Values[]) { return keys.map((key) => - this.host.dataState.find((i) => i[this.host.valueKey!] === key) + this.dataState.find((i) => i[this.host.valueKey!] === key) ); } @@ -56,7 +61,7 @@ export class SelectionController if (items.length === 0) return; items.forEach((item) => { - const i = this.host.dataState.includes(item as ComboRecord); + const i = this.dataState.includes(item as ComboRecord); if (i) { this._selected.add(item); } @@ -67,7 +72,7 @@ export class SelectionController if (items.length === 0) return; items.forEach((item) => { - const i = this.host.dataState.includes(item as ComboRecord); + const i = this.dataState.includes(item as ComboRecord); if (i) { this._selected.delete(item); } @@ -75,7 +80,7 @@ export class SelectionController } private selectAll() { - this.host.dataState + this.dataState .filter((i) => !i.header) .forEach((item) => { this._selected.add(item); @@ -169,7 +174,7 @@ export class SelectionController } public changeSelection(index: number) { - const item = this.host.dataState[index]; + const item = this.dataState[index]; if (this.host.valueKey) { !this.selected.has(item) diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 7d0a244c1..0221fd60c 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -270,11 +270,11 @@ const Template: Story = ( display-key="name" group-key="country" group-sorting="${ifDefined(groupSorting)}" - ?case-sensitive-icon=${ifDefined(caseSensitiveIcon)} - ?disable-filtering=${ifDefined(disableFiltering)} + ?case-sensitive-icon=${caseSensitiveIcon} + ?disable-filtering=${disableFiltering} ?open=${open} ?autofocus=${autofocus} - ?autofocus-list=${ifDefined(autofocusList)} + ?autofocus-list=${autofocusList} ?outlined=${outlined} ?required=${required} ?disabled=${disabled} From c6ba97717ef58553e03ef7d1f8507d9e0e160b3f Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 5 Dec 2022 19:13:50 +0200 Subject: [PATCH 73/82] refactor(combo): add item template type --- src/components/combo/combo.ts | 5 +++-- src/components/combo/types.ts | 4 +++- src/index.ts | 1 + stories/combo.stories.ts | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index adb4d64b7..3ee68b7ba 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -30,6 +30,7 @@ import { GroupingDirection, FilteringOptions, IgcComboEventMap, + ComboItemTemplate, } from './types.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { partNameMap } from '../common/util.js'; @@ -286,7 +287,7 @@ export default class IgcComboComponent * @type {(item: T) => TemplateResult} */ @property({ attribute: false }) - public itemTemplate: (item: T) => TemplateResult = (item) => { + public itemTemplate: ComboItemTemplate = (item) => { if (typeof item !== 'object' || item === null) { return String(item) as any; } @@ -303,7 +304,7 @@ export default class IgcComboComponent * @type {(item: T) => TemplateResult} */ @property({ attribute: false }) - public groupHeaderTemplate: (item: T) => TemplateResult = (item) => { + public groupHeaderTemplate: ComboItemTemplate = (item) => { return html`${this.groupKey && item[this.groupKey]}`; }; diff --git a/src/components/combo/types.ts b/src/components/combo/types.ts index cfb6d78d6..13f0a21ec 100644 --- a/src/components/combo/types.ts +++ b/src/components/combo/types.ts @@ -1,4 +1,4 @@ -import { ReactiveControllerHost } from 'lit'; +import { ReactiveControllerHost, TemplateResult } from 'lit'; import IgcComboComponent from './combo.js'; export type Keys = keyof T; @@ -44,3 +44,5 @@ export interface IgcComboEventMap { igcClosing: CustomEvent; igcClosed: CustomEvent; } + +export type ComboItemTemplate = (item: T) => TemplateResult; diff --git a/src/index.ts b/src/index.ts index 42e1b5002..fed98b8b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -81,3 +81,4 @@ export type { DatePartDeltas, } from './components/date-time-input/date-util.js'; export type { IgcRangeSliderValue } from './components/slider/range-slider.js'; +export type { ComboItemTemplate } from './components/combo/types.js'; diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 0221fd60c..0ea5d7caa 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -1,7 +1,7 @@ import { html } from 'lit'; import { Context, Story } from './story.js'; import { ifDefined } from 'lit/directives/if-defined.js'; -import { defineAllComponents } from '../src/index.js'; +import { defineAllComponents, ComboItemTemplate } from '../src/index.js'; import { registerIconFromText } from '../src/components/icon/icon.registry'; defineAllComponents(); @@ -161,7 +161,7 @@ interface City { country: string; } -const itemTemplate = (item: City) => { +const itemTemplate: ComboItemTemplate = (item: City) => { return html`
${item?.name ?? item} [${item?.zip}] @@ -169,7 +169,7 @@ const itemTemplate = (item: City) => { `; }; -const groupHeaderTemplate = (item: City) => { +const groupHeaderTemplate: ComboItemTemplate = (item: City) => { return html`
Country of ${item?.country ?? item}
`; }; From 2dddc3fa290025d1a6a35225e7d46d53b3ce2bd7 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 6 Dec 2022 08:50:19 +0200 Subject: [PATCH 74/82] fix(combo): navigation works properly --- src/components/combo/combo.ts | 8 ++++---- src/components/combo/controllers/navigation.ts | 15 +++++++-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 3ee68b7ba..3efabbb21 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -284,7 +284,7 @@ export default class IgcComboComponent /** * The template used for the content of each combo item. - * @type {(item: T) => TemplateResult} + * @type {ComboItemTemplate} */ @property({ attribute: false }) public itemTemplate: ComboItemTemplate = (item) => { @@ -301,7 +301,7 @@ export default class IgcComboComponent /** * The template used for the content of each combo group header. - * @type {(item: T) => TemplateResult} + * @type {ComboItemTemplate} */ @property({ attribute: false }) public groupHeaderTemplate: ComboItemTemplate = (item) => { @@ -336,7 +336,7 @@ export default class IgcComboComponent @watch('pipeline') protected async pipeline() { this.dataState = await this.dataController.apply([...this.data]); - this.navigationController.active = 0; + this.navigationController.active = -1; } @watch('open') @@ -565,7 +565,7 @@ export default class IgcComboComponent protected handleClearIconClick(e: MouseEvent) { e.stopPropagation(); this.selectionController.deselect([], true); - this.navigationController.active = 0; + this.navigationController.active = -1; } protected toggleCaseSensitivity() { diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts index e6ca3419c..c7dd2d24e 100644 --- a/src/components/combo/controllers/navigation.ts +++ b/src/components/combo/controllers/navigation.ts @@ -123,10 +123,7 @@ export class NavigationController protected inputArrowDown(container: IgcComboListComponent) { container.focus(); - - if (this.active === 0) { - this.active = this.firstItem; - } + this.arrowDown(container); } protected hostArrowDown() { @@ -147,8 +144,10 @@ export class NavigationController const next = this.getNearestItem(this.currentItem, direction); if (next === -1) { - if (this.active === this.lastItem) return; - this.input.focus(); + if (this.active === this.firstItem) { + this.input.focus(); + this.active = START_INDEX; + } return; } @@ -159,8 +158,8 @@ export class NavigationController let index = startIndex; const items = this.dataState; - if (items[index + direction]?.header) { - this.getNearestItem((index += direction), direction); + while (items[index + direction]?.header) { + index += direction; } index += direction; From 2ad0852619fb02db9c512f744be5f66fb28ccf2b Mon Sep 17 00:00:00 2001 From: Marin Popov Date: Tue, 6 Dec 2022 09:40:19 +0200 Subject: [PATCH 75/82] style(combo, select, input) update the styles for toggle button (#576) * style(combo, select, input) update the styles for toggle icon Co-authored-by: Simeon Simeonoff --- .../combo/themes/light/combo.material.scss | 12 ------------ .../input/themes/light/input.indigo.scss | 2 +- .../select/themes/light/select.indigo.scss | 14 +++++++++----- .../select/themes/light/select.material.scss | 12 ++++++++++++ 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/components/combo/themes/light/combo.material.scss b/src/components/combo/themes/light/combo.material.scss index ab9027d70..014caaf09 100644 --- a/src/components/combo/themes/light/combo.material.scss +++ b/src/components/combo/themes/light/combo.material.scss @@ -1,18 +1,6 @@ @use '../../../../styles/utilities' as *; @use '../../../select/themes/light/select.material'; -[part='toggle-icon'] { - background: color(gray, 300); -} - -igc-input#target:not([outlined]) { - &:focus-within { - [part='toggle-icon'] { - background: color(gray, 400, .3); - } - } -} - :host([outlined]:focus-within) { igc-input::part(suffix) { margin-inline-end: -1px; diff --git a/src/components/input/themes/light/input.indigo.scss b/src/components/input/themes/light/input.indigo.scss index f8535a740..8ce44a2a0 100644 --- a/src/components/input/themes/light/input.indigo.scss +++ b/src/components/input/themes/light/input.indigo.scss @@ -6,7 +6,7 @@ $label-focus-color: var(--label-focus-color, color(primary, 500)) !default; display: inline-flex; align-items: center; width: max-content; - height: 100%; + height: calc(100% - #{rem(2px)}); padding-inline: pad-inline(8px, 12px, 16px); } diff --git a/src/components/select/themes/light/select.indigo.scss b/src/components/select/themes/light/select.indigo.scss index cf1f5d7f7..3551a0cd0 100644 --- a/src/components/select/themes/light/select.indigo.scss +++ b/src/components/select/themes/light/select.indigo.scss @@ -1,16 +1,20 @@ -@use '../../../../styles/utilities' as utils; +@use '../../../../styles/utilities' as *; -$label-focus-color: var(--label-focus-color, utils.color(primary, 500)) !default; +$label-focus-color: var(--label-focus-color, color(primary, 500)) !default; + +[part~='toggle-icon'] { + background: color(gray, 300); +} :host { ::slotted([slot='helper-text']) { - color: utils.color(gray, 600); + color: color(gray, 600); } } :host([disabled]) { ::slotted([slot='helper-text']) { - color: utils.color(gray, 300); + color: color(gray, 300); } } @@ -24,7 +28,7 @@ $label-focus-color: var(--label-focus-color, utils.color(primary, 500)) !default :host(:not([invalid]):focus-within) { igc-input[readonly]:not([disabled])::part(container) { - background: utils.color(gray, 100); + background: color(gray, 100); } igc-input[readonly]:not([disabled])::part(label) { diff --git a/src/components/select/themes/light/select.material.scss b/src/components/select/themes/light/select.material.scss index 1ffa4cbb0..d9531d0f6 100644 --- a/src/components/select/themes/light/select.material.scss +++ b/src/components/select/themes/light/select.material.scss @@ -102,3 +102,15 @@ $active-border-width: rem(2px) !default; color: $error-color; } } + +[part~='toggle-icon'] { + background: color(gray, 300); +} + +igc-input:not([outlined]) { + &:focus-within { + [part~='toggle-icon'] { + background: color(gray, 400, .3); + } + } +} From 9164fe8395a700d4ae359bbaf58eed3aa2f2e1f8 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 6 Dec 2022 13:13:36 +0200 Subject: [PATCH 76/82] refactor(combo): pass template props --- src/components/combo/combo.ts | 8 ++++---- src/components/combo/types.ts | 7 ++++++- stories/combo.stories.ts | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 3efabbb21..c802b80ca 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -287,7 +287,7 @@ export default class IgcComboComponent * @type {ComboItemTemplate} */ @property({ attribute: false }) - public itemTemplate: ComboItemTemplate = (item) => { + public itemTemplate: ComboItemTemplate = ({ item }) => { if (typeof item !== 'object' || item === null) { return String(item) as any; } @@ -304,7 +304,7 @@ export default class IgcComboComponent * @type {ComboItemTemplate} */ @property({ attribute: false }) - public groupHeaderTemplate: ComboItemTemplate = (item) => { + public groupHeaderTemplate: ComboItemTemplate = ({ item }) => { return html`${this.groupKey && item[this.groupKey]}`; }; @@ -506,7 +506,7 @@ export default class IgcComboComponent const active = this.navigationController.active === index; const selected = this.selectionController.selected.has(item); const headerTemplate = html`${this.groupHeaderTemplate(record)}${this.groupHeaderTemplate({ item: record })}`; const itemParts = partNameMap({ @@ -522,7 +522,7 @@ export default class IgcComboComponent .index=${index} .active=${active} .selected=${selected} - >${this.itemTemplate(record)}${this.itemTemplate({ item: record })}`; return html`${this.groupKey && record.header diff --git a/src/components/combo/types.ts b/src/components/combo/types.ts index 13f0a21ec..1000f918d 100644 --- a/src/components/combo/types.ts +++ b/src/components/combo/types.ts @@ -45,4 +45,9 @@ export interface IgcComboEventMap { igcClosed: CustomEvent; } -export type ComboItemTemplate = (item: T) => TemplateResult; +export type ComboItemTemplate = ( + props: ComboTemplateProps +) => TemplateResult; +export interface ComboTemplateProps { + item: T; +} diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 0ea5d7caa..1742672b7 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -161,7 +161,7 @@ interface City { country: string; } -const itemTemplate: ComboItemTemplate = (item: City) => { +const itemTemplate: ComboItemTemplate = ({ item }) => { return html`
${item?.name ?? item} [${item?.zip}] @@ -169,7 +169,7 @@ const itemTemplate: ComboItemTemplate = (item: City) => { `; }; -const groupHeaderTemplate: ComboItemTemplate = (item: City) => { +const groupHeaderTemplate: ComboItemTemplate = ({ item }) => { return html`
Country of ${item?.country ?? item}
`; }; From 82661968b241e275e9adb1eb7281b4a38d26944b Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 6 Dec 2022 14:00:00 +0200 Subject: [PATCH 77/82] chore(combo): update changelog and _package.json --- CHANGELOG.md | 1 + scripts/_package.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f89b7eb08..a151dc6ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added - Stepper Component [#219](https://github.com/IgniteUI/igniteui-webcomponents/issues/219) +- Combo Component [#411](https://github.com/IgniteUI/igniteui-webcomponents/issues/411) ## [4.0.0] - 2022-11-02 ### Changed diff --git a/scripts/_package.json b/scripts/_package.json index b3e5abd15..f54e5b979 100644 --- a/scripts/_package.json +++ b/scripts/_package.json @@ -30,6 +30,7 @@ "circular progress", "checkbox", "chip", + "combo", "date input", "dropdown", "expansion panel", @@ -55,6 +56,7 @@ ], "dependencies": { "@floating-ui/dom": "^1.0.7", - "lit": "^2.4.1" + "lit": "^2.4.1", + "@lit-labs/virtualizer": "^0.7.2" } } From c8192f868378c2729efeda79fbf0344dc0497f9c Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 6 Dec 2022 14:42:55 +0200 Subject: [PATCH 78/82] lint(combo): fix lint errors --- src/components/combo/combo.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/combo/combo.spec.ts b/src/components/combo/combo.spec.ts index 94f156da7..8db7688f4 100644 --- a/src/components/combo/combo.spec.ts +++ b/src/components/combo/combo.spec.ts @@ -2,13 +2,11 @@ import { html } from 'lit'; import sinon from 'sinon'; import { elementUpdated, expect, fixture } from '@open-wc/testing'; import { defineComponents } from '../common/definitions/defineComponents.js'; -import IgcInputComponent from '../input/input'; +import IgcInputComponent from '../input/input.js'; import IgcComboComponent from './combo.js'; import IgcComboListComponent from './combo-list.js'; import IgcComboItemComponent from './combo-item.js'; import IgcListComponent from '../list/list.js'; -// import IgcComboListComponent from './combo-list.js'; -// import IgcComboHeaderComponent from './combo-header.js'; describe('Combo', () => { interface City { From 81eb487a70f8fcb0d6e85a5cec0d14b01b132a90 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 6 Dec 2022 16:56:38 +0200 Subject: [PATCH 79/82] refactor(combo): empty renders between header and footer --- src/components/combo/combo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index c802b80ca..04e0bf887 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -709,8 +709,8 @@ export default class IgcComboComponent ?hidden=${this.dataState.length === 0} > - ${this.renderEmptyTemplate()} +
`; } From b3e5c23cf1a39a86998c1fe8bdfac4a7a7aa38a1 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 6 Dec 2022 17:04:35 +0200 Subject: [PATCH 80/82] refactor(combo): add header and footer parts --- src/components/combo/combo.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 04e0bf887..d113429b1 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -90,6 +90,8 @@ defineComponents( * @csspart checkbox - Represents each checkbox of each list item. * @csspart checkbox-indicator - Represents the checkbox indicator of each list item. * @csspart checked - Appended to checkbox parts list when checkbox is checked. + * @csspart header - The container holding the header content. + * @csspart footer - The container holding the footer content. * @csspart empty - The container holding the empty content. */ @themes({ material, bootstrap, fluent, indigo }) @@ -699,7 +701,9 @@ export default class IgcComboComponent ${this.toggleController.toggleDirective} > ${this.renderSearchInput()} - +
+ +
> ${this.renderEmptyTemplate()} - +
+ +
`; } From 1f7098406f5785de90bc7a633f696c29ca5a6ed9 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Tue, 6 Dec 2022 17:09:45 +0200 Subject: [PATCH 81/82] fix(combo): empty part --- src/components/combo/combo.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index d113429b1..a74846be3 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -689,9 +689,9 @@ export default class IgcComboComponent } private renderEmptyTemplate() { - return html` 0}> -
The list is empty
-
`; + return html`
0}> + The list is empty +
`; } private renderList() { From 49be5a6c3887e0e589c5e8644dc6164c957dfdfb Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 7 Dec 2022 11:39:40 +0200 Subject: [PATCH 82/82] refactor(combo, select): make themeAdopted protected --- src/components/combo/combo.ts | 10 +++------- src/components/select/select.ts | 19 ++++++------------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index a74846be3..f0f1d4e0c 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -37,11 +37,7 @@ import { partNameMap } from '../common/util.js'; import { filteringOptionsConverter } from './utils/converters.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import { Constructor } from '../common/mixins/constructor.js'; -import type { - ReactiveTheme, - ThemeController, - Theme, -} from '../../theming/types.js'; +import type { ThemeController, Theme } from '../../theming/types.js'; import { blazorAdditionalDependencies } from '../common/decorators/blazorAdditionalDependencies.js'; defineComponents( @@ -102,7 +98,7 @@ export default class IgcComboComponent extends EventEmitterMixin>( LitElement ) - implements Partial, ReactiveTheme + implements Partial { public static readonly tagName = 'igc-combo'; public static styles = styles; @@ -376,7 +372,7 @@ export default class IgcComboComponent ); } - public themeAdopted(controller: ThemeController) { + protected themeAdopted(controller: ThemeController) { this.themeController = controller; } diff --git a/src/components/select/select.ts b/src/components/select/select.ts index 14fe6b3a3..3f9f11d81 100644 --- a/src/components/select/select.ts +++ b/src/components/select/select.ts @@ -22,11 +22,7 @@ import IgcInputComponent from '../input/input.js'; import IgcSelectGroupComponent from './select-group.js'; import IgcSelectHeaderComponent from './select-header.js'; import IgcSelectItemComponent from './select-item.js'; -import type { - ReactiveTheme, - ThemeController, - Theme, -} from '../../theming/types.js'; +import type { ThemeController, Theme } from '../../theming/types.js'; import { styles } from './themes/light/select.base.css.js'; import { styles as bootstrap } from './themes/light/select.bootstrap.css.js'; import { styles as fluent } from './themes/light/select.fluent.css.js'; @@ -77,13 +73,10 @@ export interface IgcSelectEventMap extends IgcDropdownEventMap { * @csspart toggle-icon - The toggle icon wrapper. * @csspart helper-text - The helper text wrapper. */ -export default class IgcSelectComponent - extends EventEmitterMixin< - IgcSelectEventMap, - Constructor - >(IgcDropdownComponent) - implements ReactiveTheme -{ +export default class IgcSelectComponent extends EventEmitterMixin< + IgcSelectEventMap, + Constructor +>(IgcDropdownComponent) { /** @private */ public static readonly tagName = 'igc-select'; public static styles = styles; @@ -215,7 +208,7 @@ export default class IgcSelectComponent }); } - public themeAdopted(controller: ThemeController) { + protected themeAdopted(controller: ThemeController) { this.themeController = controller; }