diff --git a/.github/ISSUE_TEMPLATE/accessibility.yml b/.github/ISSUE_TEMPLATE/accessibility.yml index 8a4f53634b0..1ea3c287bc7 100644 --- a/.github/ISSUE_TEMPLATE/accessibility.yml +++ b/.github/ISSUE_TEMPLATE/accessibility.yml @@ -36,7 +36,7 @@ body: attributes: label: Reproduction Sample description: | - We provide the following templates to help get started: [codepen](https://codepen.io/pen?template=RwgrjEx) (just Calcite), [codesandbox](https://codesandbox.io/s/calcite-template-p95kp?file=/src/App.js) (Calcite with React wrapper), or [jsbin](https://jsbin.com/qecewik/edit?html,output) (Calcite with ArcGIS JSAPI). You can also make your own sample using one of the those websites. Alternatively, a [documentation sample](https://developers.arcgis.com/calcite-design-system/components) can be used if the issue is reproducible. If you are experiencing build or Node related errors, please provide a GitHub repo for the sample. + We provide the following templates to help get started: [codepen](https://codepen.io/pen?template=RwgrjEx) (just Calcite), [React Codesandbox](https://codesandbox.io/s/calcite-template-p95kp?file=/src/App.js) (Calcite with React wrapper), [Angular Codesandbox](https://codesandbox.io/p/sandbox/little-fire-2hx4qh?file=%2Fsrc%2Fapp%2Fapp.component.html%3A16%2C51), or [jsbin](https://jsbin.com/qecewik/edit?html,output) (Calcite with ArcGIS JSAPI). You can also make your own sample using one of the those websites. Alternatively, a [documentation sample](https://developers.arcgis.com/calcite-design-system/components) can be used if the issue is reproducible. If you are experiencing build or Node related errors, please provide a GitHub repo for the sample. validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 2d85dc44e3c..6ce1d3be419 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -30,7 +30,7 @@ body: attributes: label: Reproduction Sample description: | - We provide the following templates to help get started: [codepen](https://codepen.io/pen?template=RwgrjEx) (just Calcite), [codesandbox](https://codesandbox.io/s/calcite-template-p95kp?file=/src/App.js) (Calcite with React wrapper), or [jsbin](https://jsbin.com/qecewik/edit?html,output) (Calcite with ArcGIS JSAPI). You can also make your own sample using one of the those websites. Alternatively, a [documentation sample](https://developers.arcgis.com/calcite-design-system/components) can be used if the issue is reproducible. If you are experiencing build or Node related errors, please provide a GitHub repo for the sample. + We provide the following templates to help get started: [codepen](https://codepen.io/pen?template=RwgrjEx) (just Calcite), [React Codesandbox](https://codesandbox.io/s/calcite-template-p95kp?file=/src/App.js) (Calcite with React wrapper), [Angular Codesandbox](https://codesandbox.io/p/sandbox/little-fire-2hx4qh?file=%2Fsrc%2Fapp%2Fapp.component.html%3A16%2C51), or [jsbin](https://jsbin.com/qecewik/edit?html,output) (Calcite with ArcGIS JSAPI). You can also make your own sample using one of the those websites. Alternatively, a [documentation sample](https://developers.arcgis.com/calcite-design-system/components) can be used if the issue is reproducible. If you are experiencing build or Node related errors, please provide a GitHub repo for the sample. validations: required: true - type: textarea diff --git a/.renovaterc.json b/.renovaterc.json index 7d7ecdfc5db..87c6dec36ef 100644 --- a/.renovaterc.json +++ b/.renovaterc.json @@ -3,6 +3,7 @@ "extends": [ "config:base", "npm:unpublishSafe", + "schedule:daily", "workarounds:typesNodeVersioning", ":pinAllExceptPeerDependencies", ":widenPeerDependencies" @@ -10,7 +11,6 @@ "platformCommit": true, "enabledManagers": ["npm"], "timezone": "America/Los_Angeles", - "schedule": ["before 5am on every weekday"], "labels": ["dependencies"], "ignoreDeps": [ "@types/jest", @@ -35,6 +35,11 @@ "semanticCommitType": "build", "semanticCommitScope": "deps", "addLabels": ["chore"] + }, + { + "matchPackagePatterns": ["^@(esri|stencil)/*"], + "schedule": ["8am", "12pm", "4pm"], + "extends": [":disableRateLimiting"] } ] } diff --git a/package-lock.json b/package-lock.json index b56ce3d54d4..a70a05ea012 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23850,11 +23850,6 @@ "node": ">= 6" } }, - "node_modules/form-request-submit-polyfill": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/form-request-submit-polyfill/-/form-request-submit-polyfill-2.0.0.tgz", - "integrity": "sha512-p0+M92y2gFnP0AuuL8VJ0GYVzAT0bYp3GsSkmPFhvUopdnfDLP/9xplQTBBc4w8qOjKRzdK7GaFcdL9IhlXdTQ==" - }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -47015,7 +47010,7 @@ }, "packages/calcite-components": { "name": "@esri/calcite-components", - "version": "2.4.0", + "version": "2.4.1-next.0", "license": "SEE LICENSE.md", "dependencies": { "@floating-ui/dom": "1.5.4", @@ -47025,7 +47020,6 @@ "composed-offset-position": "0.0.4", "dayjs": "1.11.10", "focus-trap": "7.5.4", - "form-request-submit-polyfill": "2.0.0", "lodash-es": "4.17.21", "sortablejs": "1.15.1", "timezone-groups": "0.8.0" @@ -47033,7 +47027,7 @@ "devDependencies": { "@esri/calcite-design-tokens": "^2.1.2-next.0", "@esri/calcite-ui-icons": "3.25.6", - "@esri/eslint-plugin-calcite-components": "^1.1.0", + "@esri/eslint-plugin-calcite-components": "^1.1.1-next.0", "@stencil-community/eslint-plugin": "0.7.1", "@stencil-community/postcss": "2.2.0", "@stencil/angular-output-target": "0.8.3", @@ -47066,10 +47060,10 @@ }, "packages/calcite-components-angular/projects/component-library": { "name": "@esri/calcite-components-angular", - "version": "2.4.0", + "version": "2.4.1-next.0", "license": "SEE LICENSE.md", "dependencies": { - "@esri/calcite-components": "^2.4.0", + "@esri/calcite-components": "^2.4.1-next.0", "tslib": "2.6.2" }, "peerDependencies": { @@ -47079,10 +47073,10 @@ }, "packages/calcite-components-react": { "name": "@esri/calcite-components-react", - "version": "2.4.0", + "version": "2.4.1-next.0", "license": "SEE LICENSE.md", "dependencies": { - "@esri/calcite-components": "^2.4.0" + "@esri/calcite-components": "^2.4.1-next.0" }, "peerDependencies": { "react": ">=16.7", @@ -47099,7 +47093,7 @@ }, "packages/eslint-plugin-calcite-components": { "name": "@esri/eslint-plugin-calcite-components", - "version": "1.1.0", + "version": "1.1.1-next.0", "license": "SEE LICENSE.md", "dependencies": { "stencil-eslint-core": "0.4.1" diff --git a/packages/calcite-components-angular/projects/component-library/CHANGELOG.md b/packages/calcite-components-angular/projects/component-library/CHANGELOG.md index de2bdae4fda..d873cd9c80d 100644 --- a/packages/calcite-components-angular/projects/component-library/CHANGELOG.md +++ b/packages/calcite-components-angular/projects/component-library/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.4.1-next.0](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components-angular@2.4.0...@esri/calcite-components-angular@2.4.1-next.0) (2024-02-07) + +**Note:** Version bump only for package @esri/calcite-components-angular + ## [2.4.0](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components-angular@2.3.0...@esri/calcite-components-angular@2.4.0) (2024-01-30) ### Miscellaneous Chores diff --git a/packages/calcite-components-angular/projects/component-library/package.json b/packages/calcite-components-angular/projects/component-library/package.json index 219a91b460c..9c25dd05f42 100644 --- a/packages/calcite-components-angular/projects/component-library/package.json +++ b/packages/calcite-components-angular/projects/component-library/package.json @@ -1,6 +1,6 @@ { "name": "@esri/calcite-components-angular", - "version": "2.4.0", + "version": "2.4.1-next.0", "sideEffects": false, "homepage": "https://developers.arcgis.com/calcite-design-system/", "description": "A set of Angular components that wrap Esri's Calcite Components.", @@ -20,7 +20,7 @@ "@angular/core": ">=16.0.0" }, "dependencies": { - "@esri/calcite-components": "^2.4.0", + "@esri/calcite-components": "^2.4.1-next.0", "tslib": "2.6.2" }, "lerna": { diff --git a/packages/calcite-components-react/CHANGELOG.md b/packages/calcite-components-react/CHANGELOG.md index cda207dc72f..648914c679b 100644 --- a/packages/calcite-components-react/CHANGELOG.md +++ b/packages/calcite-components-react/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.4.1-next.0](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components-react@2.4.0...@esri/calcite-components-react@2.4.1-next.0) (2024-02-07) + +**Note:** Version bump only for package @esri/calcite-components-react + ## [2.4.0](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components-react@2.3.0...@esri/calcite-components-react@2.4.0) (2024-01-30) ### Miscellaneous Chores diff --git a/packages/calcite-components-react/package.json b/packages/calcite-components-react/package.json index 5b2bb9bed9e..2a9dd122cda 100644 --- a/packages/calcite-components-react/package.json +++ b/packages/calcite-components-react/package.json @@ -1,7 +1,7 @@ { "name": "@esri/calcite-components-react", "sideEffects": false, - "version": "2.4.0", + "version": "2.4.1-next.0", "homepage": "https://developers.arcgis.com/calcite-design-system/", "description": "A set of React components that wrap calcite components", "license": "SEE LICENSE.md", @@ -23,7 +23,7 @@ "dist/" ], "dependencies": { - "@esri/calcite-components": "^2.4.0" + "@esri/calcite-components": "^2.4.1-next.0" }, "peerDependencies": { "react": ">=16.7", diff --git a/packages/calcite-components/CHANGELOG.md b/packages/calcite-components/CHANGELOG.md index 74322ede40c..948d341979f 100644 --- a/packages/calcite-components/CHANGELOG.md +++ b/packages/calcite-components/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.4.1-next.0](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components@2.4.0...@esri/calcite-components@2.4.1-next.0) (2024-02-07) + +### Bug Fixes + +- don't override existing validationMessage when displaying after form submission ([#8690](https://github.com/Esri/calcite-design-system/issues/8690)) ([3076220](https://github.com/Esri/calcite-design-system/commit/3076220bf9d463bafe5c00cd14e7246a7995ab66)), closes [#8000](https://github.com/Esri/calcite-design-system/issues/8000) [/github.com/Esri/calcite-design-system/pull/8574#discussion_r1453899164](https://github.com/Esri//github.com/Esri/calcite-design-system/pull/8574/issues/discussion_r1453899164) + ## [2.4.0](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components@2.3.0...@esri/calcite-components@2.4.0) (2024-01-30) ### Features diff --git a/packages/calcite-components/calcite-preset.ts b/packages/calcite-components/calcite-preset.ts index 59c4493fc6f..e89a48531ff 100644 --- a/packages/calcite-components/calcite-preset.ts +++ b/packages/calcite-components/calcite-preset.ts @@ -258,14 +258,17 @@ export default { "outline-color": "transparent", }, ".focus-normal": { - outline: "2px solid var(--calcite-color-brand-hover, var(--calcite--color-brand))", + outline: + "2px solid var(--calcite-ui-focus-color, var(--calcite-color-brand-hover, var(--calcite--color-brand)))", }, ".focus-outset": { - outline: "2px solid var(--calcite-color-brand)", + outline: + "2px solid var(--calcite-ui-focus-color, var(--calcite-color-brand-hover, var(--calcite--color-brand)))", "outline-offset": invert("2px", "--calcite-offset-invert-focus"), }, ".focus-inset": { - outline: "2px solid var(--calcite-color-brand)", + outline: + "2px solid var(--calcite-ui-focus-color, var(--calcite-color-brand-hover, var(--calcite--color-brand)))", "outline-offset": invert("-2px", "--calcite-offset-invert-focus"), }, ".focus-outset-danger": { diff --git a/packages/calcite-components/package.json b/packages/calcite-components/package.json index 9c8619d15b7..92d9ea06183 100644 --- a/packages/calcite-components/package.json +++ b/packages/calcite-components/package.json @@ -1,6 +1,6 @@ { "name": "@esri/calcite-components", - "version": "2.4.0", + "version": "2.4.1-next.0", "homepage": "https://developers.arcgis.com/calcite-design-system/", "description": "Web Components for Esri's Calcite Design System.", "main": "dist/index.cjs.js", @@ -69,7 +69,6 @@ "composed-offset-position": "0.0.4", "dayjs": "1.11.10", "focus-trap": "7.5.4", - "form-request-submit-polyfill": "2.0.0", "lodash-es": "4.17.21", "sortablejs": "1.15.1", "timezone-groups": "0.8.0" @@ -77,7 +76,7 @@ "devDependencies": { "@esri/calcite-design-tokens": "^2.1.2-next.0", "@esri/calcite-ui-icons": "3.25.6", - "@esri/eslint-plugin-calcite-components": "^1.1.0", + "@esri/eslint-plugin-calcite-components": "^1.1.1-next.0", "@stencil-community/eslint-plugin": "0.7.1", "@stencil-community/postcss": "2.2.0", "@stencil/angular-output-target": "0.8.3", diff --git a/packages/calcite-components/src/components/color-picker/color-picker.tsx b/packages/calcite-components/src/components/color-picker/color-picker.tsx index 5a267902bf0..e12d1c8c840 100644 --- a/packages/calcite-components/src/components/color-picker/color-picker.tsx +++ b/packages/calcite-components/src/components/color-picker/color-picker.tsx @@ -1382,7 +1382,7 @@ export class ColorPicker this.colorFieldScopeTop = y; }); - this.drawThumb(this.colorFieldRenderingContext, radius, x, y, hsvColor); + this.drawThumb(this.colorFieldRenderingContext, radius, x, y, hsvColor, false); } private drawThumb( @@ -1391,6 +1391,7 @@ export class ColorPicker x: number, y: number, color: Color, + applyAlpha: boolean, ): void { const startAngle = 0; const endAngle = 2 * Math.PI; @@ -1400,14 +1401,28 @@ export class ColorPicker context.arc(x, y, radius, startAngle, endAngle); context.fillStyle = "#fff"; context.fill(); + context.strokeStyle = "rgba(0,0,0,0.3)"; context.lineWidth = outlineWidth; context.stroke(); + if (applyAlpha && color.alpha() < 1) { + const pattern = context.createPattern(this.getCheckeredBackgroundPattern(), "repeat"); + context.beginPath(); + context.arc(x, y, radius - 3, startAngle, endAngle); + context.fillStyle = pattern; + context.fill(); + } + + context.globalCompositeOperation = "source-atop"; + context.beginPath(); context.arc(x, y, radius - 3, startAngle, endAngle); - context.fillStyle = color.rgb().alpha(1).string(); + const alpha = applyAlpha ? color.alpha() : 1; + context.fillStyle = color.rgb().alpha(alpha).string(); context.fill(); + + context.globalCompositeOperation = "source-over"; } private drawActiveHueSliderColor(): void { @@ -1434,7 +1449,7 @@ export class ColorPicker this.hueScopeLeft = sliderBoundX; }); - this.drawThumb(this.hueSliderRenderingContext, radius, sliderBoundX, y, hsvColor); + this.drawThumb(this.hueSliderRenderingContext, radius, sliderBoundX, y, hsvColor, false); } private drawHueSlider(): void { @@ -1589,7 +1604,7 @@ export class ColorPicker this.opacityScopeLeft = sliderBoundX; }); - this.drawThumb(this.opacitySliderRenderingContext, radius, sliderBoundX, y, hsvColor); + this.drawThumb(this.opacitySliderRenderingContext, radius, sliderBoundX, y, hsvColor, true); } private getSliderBoundX(x: number, width: number, radius: number): number { diff --git a/packages/calcite-components/src/components/combobox/combobox.e2e.ts b/packages/calcite-components/src/components/combobox/combobox.e2e.ts index 2ecfb7b404c..e07a1427da7 100644 --- a/packages/calcite-components/src/components/combobox/combobox.e2e.ts +++ b/packages/calcite-components/src/components/combobox/combobox.e2e.ts @@ -1609,7 +1609,7 @@ describe("calcite-combobox", () => { `, - { testValue: "two", submitsOnEnter: true }, + { testValue: "two", submitsOnEnter: true, validation: true, changeValueKeys: ["Space", "Enter"] }, ); }); diff --git a/packages/calcite-components/src/components/combobox/combobox.scss b/packages/calcite-components/src/components/combobox/combobox.scss index 58ba3dac8df..1978b19a9a4 100644 --- a/packages/calcite-components/src/components/combobox/combobox.scss +++ b/packages/calcite-components/src/components/combobox/combobox.scss @@ -142,12 +142,7 @@ .input--icon { padding-block: 0; - padding-inline: var(--calcite-combobox-item-spacing-unit-s); -} - -:host([scale="m"]) .input--icon, -:host([scale="l"]) .input--icon { - padding-inline: 0.25rem; + padding-inline: var(--calcite-combobox-item-spacing-unit-l); } .input-wrap { diff --git a/packages/calcite-components/src/components/combobox/combobox.tsx b/packages/calcite-components/src/components/combobox/combobox.tsx index 86a74ece1dc..753c3386b0a 100644 --- a/packages/calcite-components/src/components/combobox/combobox.tsx +++ b/packages/calcite-components/src/components/combobox/combobox.tsx @@ -1520,7 +1520,18 @@ export class Combobox ); } - renderInput(): VNode { + private get showingInlineIcon(): boolean { + const { placeholderIcon, selectionMode, selectedItems, open } = this; + const selectedItem = selectedItems[0]; + const selectedIcon = selectedItem?.icon; + const singleSelectionMode = isSingleLike(selectionMode); + + return !open && selectedItem + ? !!selectedIcon && singleSelectionMode + : !!placeholderIcon && (!selectedItem || singleSelectionMode); + } + + private renderInput(): VNode { const { guid, disabled, placeholder, selectionMode, selectedItems, open } = this; const single = isSingleLike(selectionMode); const selectedItem = selectedItems[0]; @@ -1554,7 +1565,7 @@ export class Combobox "input--single": true, "input--transparent": this.activeChipIndex > -1, "input--hidden": showLabel, - "input--icon": !!this.placeholderIcon, + "input--icon": this.showingInlineIcon && !!this.placeholderIcon, }} disabled={disabled} id={`${inputUidPrefix}${guid}`} @@ -1614,20 +1625,14 @@ export class Combobox ); } - renderIconStart(): VNode { - const { selectedItems, placeholderIcon, selectionMode, placeholderIconFlipRtl } = this; + renderSelectedOrPlaceholderIcon(): VNode { + const { selectedItems, placeholderIcon, placeholderIconFlipRtl } = this; const selectedItem = selectedItems[0]; const selectedIcon = selectedItem?.icon; - const singleSelectionMode = isSingleLike(selectionMode); - - const iconAtStart = - !this.open && selectedItem - ? !!selectedIcon && singleSelectionMode - : !!this.placeholderIcon && (!selectedItem || singleSelectionMode); return ( - iconAtStart && ( - + this.showingInlineIcon && ( + + + {this.renderSelectedOrPlaceholderIcon()}
- {this.renderIconStart()} {!singleSelectionMode && !singleSelectionDisplay && this.renderChips()} {!singleSelectionMode && !allSelectionDisplay && [ @@ -1714,7 +1720,7 @@ export class Combobox scale={this.scale} /> ) : null} - {this.renderIconEnd()} + {this.renderChevronIcon()}
    { describe("is form-associated", () => { describe("supports single value", () => { - formAssociated("calcite-input-date-picker", { testValue: "1985-03-23", submitsOnEnter: true }); + formAssociated("calcite-input-date-picker", { testValue: "1985-03-23", submitsOnEnter: true, validation: true }); }); describe("supports range", () => { diff --git a/packages/calcite-components/src/components/input-number/input-number.e2e.ts b/packages/calcite-components/src/components/input-number/input-number.e2e.ts index 9d7d5492786..70d7c939e21 100644 --- a/packages/calcite-components/src/components/input-number/input-number.e2e.ts +++ b/packages/calcite-components/src/components/input-number/input-number.e2e.ts @@ -1746,9 +1746,10 @@ describe("calcite-input-number", () => { describe("is form-associated", () => { formAssociated("calcite-input-number", { - testValue: 5, + testValue: "5", submitsOnEnter: true, inputType: "number", + validation: true, }); testPostValidationFocusing("calcite-input-number"); diff --git a/packages/calcite-components/src/components/input-text/input-text.e2e.ts b/packages/calcite-components/src/components/input-text/input-text.e2e.ts index 0840551a35f..0211ca26fb4 100644 --- a/packages/calcite-components/src/components/input-text/input-text.e2e.ts +++ b/packages/calcite-components/src/components/input-text/input-text.e2e.ts @@ -462,7 +462,7 @@ describe("calcite-input-text", () => { }); describe("is form-associated", () => { - formAssociated("calcite-input-text", { testValue: "test", submitsOnEnter: true }); + formAssociated("calcite-input-text", { testValue: "test", submitsOnEnter: true, validation: true }); testPostValidationFocusing("calcite-input-text"); }); diff --git a/packages/calcite-components/src/components/input-time-picker/input-time-picker.e2e.ts b/packages/calcite-components/src/components/input-time-picker/input-time-picker.e2e.ts index e2d3398c050..fbabd7beb20 100644 --- a/packages/calcite-components/src/components/input-time-picker/input-time-picker.e2e.ts +++ b/packages/calcite-components/src/components/input-time-picker/input-time-picker.e2e.ts @@ -593,7 +593,12 @@ describe("calcite-input-time-picker", () => { }); describe("is form-associated", () => { - formAssociated("calcite-input-time-picker", { testValue: "03:23", submitsOnEnter: true }); + formAssociated("calcite-input-time-picker", { + testValue: "03:23", + submitsOnEnter: true, + validation: true, + validUserInputTestValue: "03:23 AM", + }); }); it("updates value appropriately as step changes", async () => { diff --git a/packages/calcite-components/src/components/input/input.e2e.ts b/packages/calcite-components/src/components/input/input.e2e.ts index 9f02f810ff4..22ab160dcd2 100644 --- a/packages/calcite-components/src/components/input/input.e2e.ts +++ b/packages/calcite-components/src/components/input/input.e2e.ts @@ -2053,6 +2053,7 @@ describe("calcite-input", () => { testValue: value, submitsOnEnter: true, inputType: type, + validation: true, }); } diff --git a/packages/calcite-components/src/components/list-item/list-item.scss b/packages/calcite-components/src/components/list-item/list-item.scss index aee5b7569e0..0bcca595fed 100755 --- a/packages/calcite-components/src/components/list-item/list-item.scss +++ b/packages/calcite-components/src/components/list-item/list-item.scss @@ -201,7 +201,8 @@ td:focus { } } -::slotted(calcite-list-item) { +::slotted(calcite-list-item), +::slotted(calcite-list) { @apply border-solid border-0 border-t border-color-3; } diff --git a/packages/calcite-components/src/components/list/list.stories.ts b/packages/calcite-components/src/components/list/list.stories.ts index 5e4458ea756..2682ab042b8 100644 --- a/packages/calcite-components/src/components/list/list.stories.ts +++ b/packages/calcite-components/src/components/list/list.stories.ts @@ -934,3 +934,23 @@ export const filteredListItemsNoResults_TestOnly = (): string =>
    Try a different fruit?
    `; + +export const nestingLists_TestOnly = (): string => html`

    Nesting List Items

    + + + + + + + +
    +

    Nesting Lists

    + + + + + + + + + `; diff --git a/packages/calcite-components/src/components/select/select.e2e.ts b/packages/calcite-components/src/components/select/select.e2e.ts index 8deac1487ed..0ab52454c9e 100644 --- a/packages/calcite-components/src/components/select/select.e2e.ts +++ b/packages/calcite-components/src/components/select/select.e2e.ts @@ -406,12 +406,13 @@ describe("calcite-select", () => { formAssociated( html` + uno dos tres `, - { testValue: "dos" }, + { testValue: "dos", validation: true, changeValueKeys: ["ArrowDown"] }, ); }); }); diff --git a/packages/calcite-components/src/components/text-area/text-area.e2e.ts b/packages/calcite-components/src/components/text-area/text-area.e2e.ts index 255cf87f099..a9be384c767 100644 --- a/packages/calcite-components/src/components/text-area/text-area.e2e.ts +++ b/packages/calcite-components/src/components/text-area/text-area.e2e.ts @@ -99,6 +99,7 @@ describe("calcite-text-area", () => { testValue: "zion national park", expectedSubmitValue: "zion national park", submitsOnEnter: false, + validation: true, }); }); diff --git a/packages/calcite-components/src/tests/commonTests.ts b/packages/calcite-components/src/tests/commonTests.ts index 8ccbc4027be..02e658744f7 100644 --- a/packages/calcite-components/src/tests/commonTests.ts +++ b/packages/calcite-components/src/tests/commonTests.ts @@ -6,7 +6,7 @@ import { toHaveNoViolations } from "jest-axe"; import { config } from "../../stencil.config"; import { html } from "../../support/formatting"; import type { JSX } from "../components"; -import { hiddenFormInputSlotName } from "../utils/form"; +import { getClearValidationEventName, hiddenFormInputSlotName, componentsWithInputEvent } from "../utils/form"; import { MessageBundle } from "../utils/t9n"; import { GlobalTestProps, @@ -15,6 +15,7 @@ import { newProgrammaticE2EPage, skipAnimations, } from "./utils"; +import { KeyInput } from "puppeteer"; expect.extend(toHaveNoViolations); @@ -685,10 +686,29 @@ interface FormAssociatedOptions { testValue: any; /** - * Set this if the expected submit value **is different** from stringifying `testValue`. For example, a component may transform an object to a serializable string. + * Set this if the expected submit value **is different** from stringifying `testValue`. + * For example, a component may transform an object to a serializable string. */ expectedSubmitValue?: any; + /* + * Set this if the value required to emit an input/change event is different from `testValue`. + * The value is passed to `page.keyboard.type()`. For example, input-time-picker requires + * appending AM or PM before the value commits and calciteInputTimePickerChange emits. + * + * This option is only relevant when the `validation` option is enabled. + */ + validUserInputTestValue?: string; + + /* + * Set this if emitting an input/change event requires key presses. Each array item will be passed + * to `page.keyboard.press()`. For example, the combobox value can be changed by pressing "Space" + * to open the component and "Enter" to select a value. + * + * This option is only relevant when the `validation` option is enabled. + */ + changeValueKeys?: KeyInput[]; + /** * Specifies the input type that will be used to capture the value. */ @@ -703,6 +723,11 @@ interface FormAssociatedOptions { * Specifies if the component supports clearing its value (i.e., setting to null). */ clearable?: boolean; + + /** + * Specifies if the component supports preventing submission and displaying validation messages. + */ + validation?: boolean; } /** @@ -717,8 +742,14 @@ export function formAssociated( componentTagOrHtml: TagOrHTML | TagOrHTMLWithBeforeContent, options: FormAssociatedOptions, ): void { - it("supports association via ancestry", () => testAncestorFormAssociated()); - it("supports association via form ID", () => testIdFormAssociated()); + const inputTypeContext = options?.inputType ? ` (input type="${options.inputType}")` : ""; + + it(`supports association via ancestry${inputTypeContext}`, () => testAncestorFormAssociated()); + it(`supports association via form ID${inputTypeContext}`, () => testIdFormAssociated()); + + if (options?.validation && !["color", "month", "time"].includes(options?.inputType)) { + it(`supports required property validation${inputTypeContext}`, () => testRequiredPropertyValidation()); + } async function testAncestorFormAssociated(): Promise { const { beforeContent, tagOrHTML } = getTagOrHTMLWithBeforeContent(componentTagOrHtml); @@ -777,14 +808,52 @@ export function formAssociated( } } - function ensureForm(html: string, componentTag: string): string { - return html.includes("form=") ? html : html.replace(componentTag, `${componentTag} form="test-form" `); + async function testRequiredPropertyValidation(): Promise { + const requiredValidationMessage = "Please fill out this field."; + const { beforeContent, tagOrHTML } = getTagOrHTMLWithBeforeContent(componentTagOrHtml); + const tag = getTag(tagOrHTML); + const componentHtml = ensureUnchecked( + ensureRequired(ensureName(isHTML(tagOrHTML) ? tagOrHTML : `<${tag}>`, tag), tag), + ); + + const page = await newE2EPage(); + await beforeContent?.(page); + + const content = html` +
    + ${componentHtml} + Submit +
    + `; + + await page.setContent(content); + await page.waitForChanges(); + const component = await page.find(tag); + + const submitButton = await page.find("#submitButton"); + const spyEvent = await page.spyOnEvent(getClearValidationEventName(tag)); + + await assertPreventsFormSubmission(page, component, submitButton, requiredValidationMessage); + await assertClearsValidationOnValueChange(page, component, options, spyEvent, tag); + await assertUserMessageNotOverridden(page, component, submitButton); } function ensureName(html: string, componentTag: string): string { return html.includes("name=") ? html : html.replace(componentTag, `${componentTag} name="testName" `); } + function ensureRequired(html: string, componentTag: string): string { + return html.includes("required") ? html : html.replace(componentTag, `${componentTag} required `); + } + + function ensureUnchecked(html: string): string { + return html.replace(/(checked|selected)/, ""); + } + + function ensureForm(html: string, componentTag: string): string { + return html.includes("form=") ? html : html.replace(componentTag, `${componentTag} form="test-form" `); + } + async function isCheckable(page: E2EPage, component: E2EElement, options: FormAssociatedOptions): Promise { return ( typeof options.testValue === "boolean" && @@ -983,6 +1052,74 @@ export function formAssociated( expect(called).toBe(true); } + + async function assertPreventsFormSubmission( + page: E2EPage, + component: E2EElement, + submitButton: E2EElement, + message: string, + ) { + await submitButton.click(); + await page.waitForChanges(); + + await expectValidationInvalid(component, message); + } + + async function assertClearsValidationOnValueChange( + page: E2EPage, + component: E2EElement, + options: FormAssociatedOptions, + event: EventSpy, + tag: string, + ) { + if (options?.changeValueKeys) { + for (const key of options.changeValueKeys) { + await page.keyboard.press(key); + } + } else { + await page.keyboard.type(options?.validUserInputTestValue ?? options.testValue); + await page.keyboard.press("Tab"); + } + + await page.waitForChanges(); + + // components with an Input event will emit multiple times depending on the length of testValue + if (componentsWithInputEvent.includes(tag)) { + expect(event.length).toBeGreaterThanOrEqual(1); + } else { + expect(event).toHaveReceivedEventTimes(1); + } + + await expectValidationIdle(component); + } + + async function assertUserMessageNotOverridden(page: E2EPage, component: E2EElement, submitButton: E2EElement) { + const customValidationMessage = "This is a custom message."; + const customValidationIcon = "banana"; + + // don't override custom validation message and icon + component.setProperty("validationMessage", customValidationMessage); + component.setProperty("validationIcon", customValidationIcon); + component.setProperty("value", undefined); + await page.waitForChanges(); + + await submitButton.click(); + await page.waitForChanges(); + + await expectValidationInvalid(component, customValidationMessage, customValidationIcon); + } + + async function expectValidationIdle(element: E2EElement) { + expect(await element.getProperty("status")).toBe("idle"); + expect(await element.getProperty("validationMessage")).toBe(""); + expect(await element.getProperty("validationIcon")).toBe(false); + } + + async function expectValidationInvalid(element: E2EElement, message: string, icon: string = "") { + expect(await element.getProperty("status")).toBe("invalid"); + expect(await element.getProperty("validationMessage")).toBe(message); + expect(element.getAttribute("validation-icon")).toBe(icon); + } } interface TabAndClickTargets { diff --git a/packages/calcite-components/src/utils/form.e2e.ts b/packages/calcite-components/src/utils/form.e2e.ts deleted file mode 100644 index 96fe0980d2b..00000000000 --- a/packages/calcite-components/src/utils/form.e2e.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { E2EElement, newE2EPage } from "@stencil/core/testing"; -import { html } from "../../support/formatting"; -import { componentsWithInputEvent } from "./form"; - -async function assertValidationIdle(element: E2EElement) { - expect(await element.getProperty("status")).toBe("idle"); - expect(await element.getProperty("validationMessage")).toBe(""); - expect(await element.getProperty("validationIcon")).toBe(false); -} - -async function assertValidationInvalid(element: E2EElement, message: string) { - expect(await element.getProperty("status")).toBe("invalid"); - expect(await element.getProperty("validationMessage")).toBe(message); - expect(element.getAttribute("validation-icon")).toBe(""); -} - -describe("form", () => { - describe("constraint validation", () => { - describe("required property", () => { - const requiredValidationMessage = "Please fill out this field."; - - const getInputEventName = (component: string): string => - component - .split("-") - .map((part: string, index: number) => (index === 0 ? part : `${part[0].toUpperCase()}${part.slice(1)}`)) - .join("") - .concat("Input"); - - for (const component of ["calcite-input", "calcite-input-number", "calcite-input-text"]) { - it(`${component} - enter to submit`, async () => { - const page = await newE2EPage(); - await page.setContent(html` -
    - <${component} required name="${component}"> -
    - `); - - const element = await page.find(component); - - const clearValidationEventName = getInputEventName(component); - const inputEvent = await page.spyOnEvent(clearValidationEventName); - - await element.callMethod("setFocus"); - await page.waitForChanges(); - - await page.keyboard.press("Enter"); - await page.waitForChanges(); - - await assertValidationInvalid(element, requiredValidationMessage); - - await page.keyboard.press("1"); - await page.waitForChanges(); - - expect(inputEvent).toHaveReceivedEventTimes(1); - expect(await element.getProperty("value")).toBe("1"); - - await assertValidationIdle(element); - }); - } - - for (const component of componentsWithInputEvent) { - it(`${component}`, async () => { - const page = await newE2EPage(); - await page.setContent(html` -
    - <${component} required name="${component}"> - Submit -
    - `); - - const submitButton = await page.find("calcite-button"); - const element = await page.find(component); - - const clearValidationEventName = getInputEventName(component); - const inputEvent = await page.spyOnEvent(clearValidationEventName); - - await submitButton.click(); - await page.waitForChanges(); - - await assertValidationInvalid(element, requiredValidationMessage); - - await element.callMethod("setFocus"); - await page.waitForChanges(); - - await page.keyboard.press("1"); - await page.waitForChanges(); - - expect(inputEvent).toHaveReceivedEventTimes(1); - expect(await element.getProperty("value")).toBe("1"); - - await assertValidationIdle(element); - }); - } - - it(`calcite-input-date-picker`, async () => { - const page = await newE2EPage(); - await page.setContent(html` -
    - - Submit -
    - `); - - const submitButton = await page.find("calcite-button"); - const element = await page.find("calcite-input-date-picker"); - const changeEvent = await page.spyOnEvent("calciteInputDatePickerChange"); - - await submitButton.click(); - await page.waitForChanges(); - - await assertValidationInvalid(element, requiredValidationMessage); - - await element.callMethod("setFocus"); - await page.waitForChanges(); - - await page.keyboard.type("12/12/2012"); - await page.keyboard.press("Tab"); - await page.waitForChanges(); - - expect(changeEvent).toHaveReceivedEventTimes(1); - - await assertValidationIdle(element); - }); - - it(`calcite-input-time-picker`, async () => { - const page = await newE2EPage(); - await page.setContent(html` -
    - - Submit -
    - `); - - const submitButton = await page.find("calcite-button"); - const element = await page.find("calcite-input-time-picker"); - const changeEvent = await page.spyOnEvent("calciteInputTimePickerChange"); - - await submitButton.click(); - await page.waitForChanges(); - - await assertValidationInvalid(element, requiredValidationMessage); - - await element.callMethod("setFocus"); - await page.waitForChanges(); - - await page.keyboard.type("12:00 PM"); - await page.keyboard.press("Tab"); - await page.waitForChanges(); - - expect(changeEvent).toHaveReceivedEventTimes(1); - expect(await element.getProperty("value")).toBe("12:00"); - - await assertValidationIdle(element); - }); - - it(`calcite-select`, async () => { - const page = await newE2EPage(); - await page.setContent(html` -
    - - - uno - dos - tres - - Submit -
    - `); - - const submitButton = await page.find("calcite-button"); - const element = await page.find("calcite-select"); - const changeEvent = await page.spyOnEvent("calciteSelectChange"); - - await submitButton.click(); - await page.waitForChanges(); - - await assertValidationInvalid(element, requiredValidationMessage); - - await element.callMethod("setFocus"); - await page.waitForChanges(); - - await page.keyboard.press("ArrowDown"); - await page.waitForChanges(); - - expect(changeEvent).toHaveReceivedEventTimes(1); - expect(await element.getProperty("value")).toBe("uno"); - - await assertValidationIdle(element); - }); - - it(`calcite-combobox`, async () => { - const page = await newE2EPage(); - await page.setContent(html` -
    - - - - Submit -
    - `); - - const submitButton = await page.find("calcite-button"); - const element = await page.find("calcite-combobox"); - const changeEvent = await page.spyOnEvent("calciteComboboxChange"); - - await submitButton.click(); - await page.waitForChanges(); - - await assertValidationInvalid(element, requiredValidationMessage); - - await element.callMethod("setFocus"); - await page.waitForChanges(); - - await page.keyboard.press("Space"); - await page.keyboard.press("Enter"); - await page.waitForChanges(); - - expect(changeEvent).toHaveReceivedEventTimes(1); - expect(await element.getProperty("value")).toBe("Pine"); - await assertValidationIdle(element); - }); - - it.skip(`calcite-radio-button-group`, async () => { - const page = await newE2EPage(); - await page.setContent(html` -
    - - - 1 - - - - 2 - - - - 3 - - - - Submit -
    - `); - - const submitButton = await page.find("calcite-button"); - const element = await page.find("calcite-radio-button-group"); - const changeEvent = await page.spyOnEvent("calciteRadioButtonGroupChange"); - - await submitButton.click(); - await page.waitForChanges(); - - await assertValidationInvalid(element, requiredValidationMessage); - - await element.callMethod("setFocus"); - await page.waitForChanges(); - - await page.keyboard.press("Space"); - await page.waitForChanges(); - - expect(changeEvent).toHaveReceivedEventTimes(1); - expect(await element.getProperty("value")).toBe("1"); - - await assertValidationIdle(element); - }); - - it.skip(`calcite-segmented-control`, async () => { - const page = await newE2EPage(); - await page.setContent(html` -
    - - 1 - 2 - 3 - - Submit -
    - `); - - const submitButton = await page.find("calcite-button"); - const element = await page.find("calcite-segmented-control"); - const changeEvent = await page.spyOnEvent("calciteSegmentedControlChange"); - - await submitButton.click(); - await page.waitForChanges(); - - await assertValidationInvalid(element, requiredValidationMessage); - - await element.callMethod("setFocus"); - await page.waitForChanges(); - - await page.keyboard.press("Space"); - await page.waitForChanges(); - - expect(changeEvent).toHaveReceivedEventTimes(1); - expect(await element.getProperty("value")).toBe("1"); - - await assertValidationIdle(element); - }); - }); - }); -}); diff --git a/packages/calcite-components/src/utils/form.tsx b/packages/calcite-components/src/utils/form.tsx index 1476bf9dd29..d46c5d1c0e7 100644 --- a/packages/calcite-components/src/utils/form.tsx +++ b/packages/calcite-components/src/utils/form.tsx @@ -3,7 +3,6 @@ import { FunctionalComponent, h } from "@stencil/core"; /** * Any form with a `calciteInput` event needs to be included in this array. - * Exported for testing purposes. */ export const componentsWithInputEvent = [ "calcite-input", @@ -12,6 +11,32 @@ export const componentsWithInputEvent = [ "calcite-text-area", ]; +/** + * Get the event name to listen for that, when emitted, will clear the + * validation message that displays after form submission. Only validation + * messages that are set by the browser will be cleared. If a user sets + * validationMessage to a custom value, they are responsible for clearing it. + * + * Exported for testing purposes. + * + * @param componentTag the tag of the component, e.g. "calcite-input" + * @returns the event name + */ +export function getClearValidationEventName(componentTag: string): string { + const componentTagCamelCase = componentTag + .split("-") + .map((part: string, index: number) => + index === 0 ? part : `${part[0].toUpperCase()}${part.slice(1)}`, + ) + .join(""); + + const clearValidationEvent = `${componentTagCamelCase}${ + componentsWithInputEvent.includes(componentTag) ? "Input" : "Change" + }`; + + return clearValidationEvent; +} + /** * Exported for testing purposes. */ @@ -180,8 +205,12 @@ function setInvalidFormValidation( message: string, ): void { "status" in component && (component.status = "invalid"); - "validationIcon" in component && (component.validationIcon = true); - "validationMessage" in component && (component.validationMessage = message); + + "validationIcon" in component && !component.validationIcon && (component.validationIcon = true); + + "validationMessage" in component && + !component.validationMessage && + (component.validationMessage = message); } function displayValidationMessage(event: Event) { @@ -201,18 +230,13 @@ function displayValidationMessage(event: Event) { // prevent the browser from showing the native validation popover event?.preventDefault(); - setInvalidFormValidation(formComponent, hiddenInput?.validationMessage || ""); - - const componentTagCamelCase = componentTagParts - .map((part: string, index: number) => - index === 0 ? part : `${part[0].toUpperCase()}${part.slice(1)}`, - ) - .join(""); + setInvalidFormValidation(formComponent, hiddenInput?.validationMessage); - const clearValidationEvent = `${componentTagCamelCase}${ - componentsWithInputEvent.includes(componentTag) ? "Input" : "Change" - }`; + if (formComponent?.validationMessage !== hiddenInput?.validationMessage) { + return; + } + const clearValidationEvent = getClearValidationEventName(componentTag); formComponent.addEventListener(clearValidationEvent, () => clearFormValidation(formComponent), { once: true, }); diff --git a/packages/eslint-plugin-calcite-components/CHANGELOG.md b/packages/eslint-plugin-calcite-components/CHANGELOG.md index 67959fb6bb8..8750ae2e06a 100644 --- a/packages/eslint-plugin-calcite-components/CHANGELOG.md +++ b/packages/eslint-plugin-calcite-components/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.1.1-next.0](https://github.com/Esri/calcite-design-system/compare/@esri/eslint-plugin-calcite-components@1.1.0...@esri/eslint-plugin-calcite-components@1.1.1-next.0) (2024-02-07) + +**Note:** Version bump only for package @esri/eslint-plugin-calcite-components + ## [1.1.0](https://github.com/Esri/calcite-design-system/compare/@esri/eslint-plugin-calcite-components@1.0.0...@esri/eslint-plugin-calcite-components@1.1.0) (2024-01-30) ### Features diff --git a/packages/eslint-plugin-calcite-components/package.json b/packages/eslint-plugin-calcite-components/package.json index 0a61470d46e..14cbcfd3ef6 100644 --- a/packages/eslint-plugin-calcite-components/package.json +++ b/packages/eslint-plugin-calcite-components/package.json @@ -1,6 +1,6 @@ { "name": "@esri/eslint-plugin-calcite-components", - "version": "1.1.0", + "version": "1.1.1-next.0", "description": "ESLint rules for @esri/calcite-components", "main": "dist/index.js", "files": [