diff --git a/packages/calcite-components/src/components.d.ts b/packages/calcite-components/src/components.d.ts index 0eb3a842a58..b058ecc2517 100644 --- a/packages/calcite-components/src/components.d.ts +++ b/packages/calcite-components/src/components.d.ts @@ -3796,6 +3796,18 @@ export namespace Components { * Sets focus on the fist focusable `calcite-radio-button` element in the component. */ "setFocus": () => Promise; + /** + * Specifies the status of the validation message. + */ + "status": Status; + /** + * Specifies the validation icon to display under the component. + */ + "validationIcon": string | boolean; + /** + * Specifies the validation message to display under the component. + */ + "validationMessage": string; } interface CalciteRating { /** @@ -3903,6 +3915,18 @@ export namespace Components { * Sets focus on the component. */ "setFocus": () => Promise; + /** + * Specifies the status of the validation message. + */ + "status": Status; + /** + * Specifies the validation icon to display under the component. + */ + "validationIcon": string | boolean; + /** + * Specifies the validation message to display under the component. + */ + "validationMessage": string; /** * The component's `selectedItem` value. */ @@ -11250,6 +11274,18 @@ declare namespace LocalJSX { * @readonly */ "selectedItem"?: HTMLCalciteRadioButtonElement; + /** + * Specifies the status of the validation message. + */ + "status"?: Status; + /** + * Specifies the validation icon to display under the component. + */ + "validationIcon"?: string | boolean; + /** + * Specifies the validation message to display under the component. + */ + "validationMessage"?: string; } interface CalciteRating { /** @@ -11357,6 +11393,18 @@ declare namespace LocalJSX { * @readonly */ "selectedItem"?: HTMLCalciteSegmentedControlItemElement; + /** + * Specifies the status of the validation message. + */ + "status"?: Status; + /** + * Specifies the validation icon to display under the component. + */ + "validationIcon"?: string | boolean; + /** + * Specifies the validation message to display under the component. + */ + "validationMessage"?: string; /** * The component's `selectedItem` value. */ diff --git a/packages/calcite-components/src/components/radio-button-group/radio-button-group.e2e.ts b/packages/calcite-components/src/components/radio-button-group/radio-button-group.e2e.ts index f8af4a95042..0944a369c0c 100644 --- a/packages/calcite-components/src/components/radio-button-group/radio-button-group.e2e.ts +++ b/packages/calcite-components/src/components/radio-button-group/radio-button-group.e2e.ts @@ -18,6 +18,9 @@ describe("calcite-radio-button-group", () => { defaults("calcite-radio-button-group", [ { propertyName: "layout", defaultValue: "horizontal" }, { propertyName: "scale", defaultValue: "m" }, + { propertyName: "status", defaultValue: "idle" }, + { propertyName: "validationIcon", defaultValue: undefined }, + { propertyName: "validationMessage", defaultValue: undefined }, ]); }); @@ -108,6 +111,8 @@ describe("calcite-radio-button-group", () => { { propertyName: "name", value: "reflects-name" }, { propertyName: "required", value: true }, { propertyName: "scale", value: "m" }, + { propertyName: "status", value: "invalid" }, + { propertyName: "validationIcon", value: true }, ]); }); diff --git a/packages/calcite-components/src/components/radio-button-group/radio-button-group.scss b/packages/calcite-components/src/components/radio-button-group/radio-button-group.scss index e0bdc527bce..62d1727b333 100644 --- a/packages/calcite-components/src/components/radio-button-group/radio-button-group.scss +++ b/packages/calcite-components/src/components/radio-button-group/radio-button-group.scss @@ -1,21 +1,43 @@ :host { + @apply flex flex-col; +} + +:host > .item-wrapper { @apply flex; max-inline-size: 100vw; } -:host([layout="horizontal"]) { + +:host([layout="horizontal"]) > .item-wrapper { @apply flex-row flex-wrap; } -:host([layout="horizontal"][scale="s"]) { - @apply gap-4; + +:host([layout="horizontal"][scale="s"]) > .item-wrapper { + @apply gap-x-4; } -:host([layout="horizontal"][scale="m"]) { - @apply gap-5; + +:host([layout="horizontal"][scale="m"]) > .item-wrapper { + @apply gap-x-5; } -:host([layout="horizontal"][scale="l"]) { - @apply gap-6; + +:host([layout="horizontal"][scale="l"]) > .item-wrapper { + @apply gap-x-6; } -:host([layout="vertical"]) { + +:host([layout="vertical"]) > .item-wrapper { @apply flex-col; } +:host([scale="s"]) calcite-input-message { + --calcite-input-message-spacing-value: calc(var(--calcite-spacing-xxs) * -1); +} + +:host([scale="m"]) calcite-input-message { + --calcite-input-message-spacing-value: calc(var(--calcite-spacing-sm) * -1); +} + +:host([scale="l"]) calcite-input-message { + --calcite-input-message-spacing-value: calc(var(--calcite-spacing-md) * -1); +} + +@include form-validation-message(); @include base-component(); diff --git a/packages/calcite-components/src/components/radio-button-group/radio-button-group.stories.ts b/packages/calcite-components/src/components/radio-button-group/radio-button-group.stories.ts index 06d774bcb75..3e9517d83e7 100644 --- a/packages/calcite-components/src/components/radio-button-group/radio-button-group.stories.ts +++ b/packages/calcite-components/src/components/radio-button-group/radio-button-group.stories.ts @@ -1,8 +1,8 @@ -import { select } from "@storybook/addon-knobs"; -import { boolean, storyFilters } from "../../../.storybook/helpers"; +import { select, text } from "@storybook/addon-knobs"; +import { boolean, iconNames, storyFilters } from "../../../.storybook/helpers"; import { modesDarkDefault } from "../../../.storybook/utils"; -import readme from "./readme.md"; import { html } from "../../../support/formatting"; +import readme from "./readme.md"; export default { title: "Components/Controls/Radio/Radio Button Group", @@ -39,15 +39,20 @@ export const simple = (): string => html` `; -export const darkModeRTL_TestOnly = (): string => html` +// We created a separate story for validation-message because it is not possible +// to set a text knob's value to undefined. Unfortunately, this makes the CSS +// attribute selector truthy, which sets "--calcite-label-margin-bottom: 0;", +// causing a Chromatic diff. See calcite-radio-button-group.scss. +export const validationMessage_NoTest = (): string => html` @@ -68,4 +73,115 @@ export const darkModeRTL_TestOnly = (): string => html` `; +export const darkModeRTL_TestOnly = (): string => html` + + + + React + + + + Ember + + + + Angular + + + + Vue + + +`; + darkModeRTL_TestOnly.parameters = { modes: modesDarkDefault }; + +export const validationMessage_TestOnly = (): string => html` + +
+ + + + One + + + + Two + + + + Three + + + + + + + One + + + + Two + + + + Three + + + + + + + One + + + + Two + + + + Three + + +
+`; diff --git a/packages/calcite-components/src/components/radio-button-group/radio-button-group.tsx b/packages/calcite-components/src/components/radio-button-group/radio-button-group.tsx index dbaf09d09cc..e2184aefac8 100644 --- a/packages/calcite-components/src/components/radio-button-group/radio-button-group.tsx +++ b/packages/calcite-components/src/components/radio-button-group/radio-button-group.tsx @@ -13,13 +13,15 @@ import { Watch, } from "@stencil/core"; import { createObserver } from "../../utils/observers"; -import { Layout, Scale } from "../interfaces"; +import { Layout, Scale, Status } from "../interfaces"; import { componentFocusable, LoadableComponent, setComponentLoaded, setUpLoadableComponent, } from "../../utils/loadable"; +import { Validation } from "../functional/Validation"; +import { CSS } from "./resources"; /** * @slot - A slot for adding `calcite-radio-button`s. @@ -76,6 +78,15 @@ export class RadioButtonGroup implements LoadableComponent { /** Specifies the size of the component. */ @Prop({ reflect: true }) scale: Scale = "m"; + /** Specifies the status of the validation message. */ + @Prop({ reflect: true }) status: Status = "idle"; + + /** Specifies the validation message to display under the component. */ + @Prop() validationMessage: string; + + /** Specifies the validation icon to display under the component. */ + @Prop({ reflect: true }) validationIcon: string | boolean; + @Watch("scale") onScaleChange(): void { this.passPropsToRadioButtons(); @@ -191,7 +202,17 @@ export class RadioButtonGroup implements LoadableComponent { render(): VNode { return ( - +
+ +
+ {this.validationMessage ? ( + + ) : null}
); } diff --git a/packages/calcite-components/src/components/radio-button-group/resources.ts b/packages/calcite-components/src/components/radio-button-group/resources.ts new file mode 100644 index 00000000000..3cec180ba48 --- /dev/null +++ b/packages/calcite-components/src/components/radio-button-group/resources.ts @@ -0,0 +1,3 @@ +export const CSS = { + itemWrapper: "item-wrapper", +}; diff --git a/packages/calcite-components/src/components/segmented-control/resources.ts b/packages/calcite-components/src/components/segmented-control/resources.ts new file mode 100644 index 00000000000..3cec180ba48 --- /dev/null +++ b/packages/calcite-components/src/components/segmented-control/resources.ts @@ -0,0 +1,3 @@ +export const CSS = { + itemWrapper: "item-wrapper", +}; diff --git a/packages/calcite-components/src/components/segmented-control/segmented-control.e2e.ts b/packages/calcite-components/src/components/segmented-control/segmented-control.e2e.ts index 39b2ac112a2..d7f6a36215c 100644 --- a/packages/calcite-components/src/components/segmented-control/segmented-control.e2e.ts +++ b/packages/calcite-components/src/components/segmented-control/segmented-control.e2e.ts @@ -31,6 +31,18 @@ describe("calcite-segmented-control", () => { propertyName: "width", defaultValue: "auto", }, + { + propertyName: "status", + defaultValue: "idle", + }, + { + propertyName: "validationIcon", + defaultValue: undefined, + }, + { + propertyName: "validationMessage", + defaultValue: undefined, + }, ]); }); @@ -52,6 +64,14 @@ describe("calcite-segmented-control", () => { propertyName: "width", value: "auto", }, + { + propertyName: "status", + value: "invalid", + }, + { + propertyName: "validationIcon", + value: true, + }, ]); }); diff --git a/packages/calcite-components/src/components/segmented-control/segmented-control.scss b/packages/calcite-components/src/components/segmented-control/segmented-control.scss index 54c7e164313..aee1e1863d8 100644 --- a/packages/calcite-components/src/components/segmented-control/segmented-control.scss +++ b/packages/calcite-components/src/components/segmented-control/segmented-control.scss @@ -1,33 +1,40 @@ :host { + @apply flex flex-col; +} + +.item-wrapper { @apply bg-foreground-1 flex; inline-size: fit-content; outline: 1px solid var(--calcite-color-border-input); outline-offset: -1px; } -:host([appearance="outline"]) { +:host([appearance="outline"]) > .item-wrapper { @apply bg-transparent; } @include disabled(); -:host([layout="vertical"]) { +:host([layout="vertical"]) > .item-wrapper { @apply flex-col items-start self-start; } // segmented control width for full - -:host([width="full"]) { +:host([width="full"]) > .item-wrapper { @apply w-full; min-inline-size: fit-content; + ::slotted(calcite-segmented-control-item) { @apply flex-auto; } } -:host([width="full"][layout="vertical"]) ::slotted(calcite-segmented-control-item) { - @apply justify-start; +:host([width="full"][layout="vertical"]) > .item-wrapper { + ::slotted(calcite-segmented-control-item) { + @apply justify-start; + } } +@include form-validation-message(); @include hidden-form-input(); @include base-component(); diff --git a/packages/calcite-components/src/components/segmented-control/segmented-control.stories.ts b/packages/calcite-components/src/components/segmented-control/segmented-control.stories.ts index bca3d1ae779..c442690a2cc 100644 --- a/packages/calcite-components/src/components/segmented-control/segmented-control.stories.ts +++ b/packages/calcite-components/src/components/segmented-control/segmented-control.stories.ts @@ -1,5 +1,5 @@ -import { select } from "@storybook/addon-knobs"; -import { boolean, storyFilters } from "../../../.storybook/helpers"; +import { select, text } from "@storybook/addon-knobs"; +import { boolean, iconNames, storyFilters } from "../../../.storybook/helpers"; import { modesDarkDefault } from "../../../.storybook/utils"; import { html } from "../../../support/formatting"; import readme2 from "../segmented-control-item/readme.md"; @@ -20,6 +20,9 @@ export const simple = (): string => html` scale="${select("scale", ["s", "m", "l"], "m")}" width="${select("width", ["auto", "full"], "auto")}" ${boolean("disabled", false)} + status="${select("status", ["idle", "invalid", "valid"], "idle")}" + validation-icon="${select("validation-icon", ["", ...iconNames], "")}" + validation-message="${text("validation-message", "")}" > React Ember @@ -37,6 +40,9 @@ export const fullWidthWithIcons = (): string => html` appearance="${select("appearance", ["solid", "outline", "outline-fill"], "solid")}" width="${select("width", ["auto", "full"], "full")}" ${boolean("disabled", false)} + status="${select("status", ["idle", "invalid", "valid"], "idle")}" + validation-icon="${select("validation-icon", ["", ...iconNames], "")}" + validation-message="${text("validation-message", "")}" > Car Plane @@ -47,15 +53,7 @@ export const fullWidthWithIcons = (): string => html` `; export const darkThemeRTL_TestOnly = (): string => html` - + React Ember Angular @@ -86,3 +84,58 @@ export const WithIconStartAndEnd = (): string => > Nothing `; + +export const validationMessage_TestOnly = (): string => html` + +
+ + React + Ember + Angular + Vue + + + + React + Ember + Angular + Vue + + + + React + Ember + Angular + Vue + +
+`; diff --git a/packages/calcite-components/src/components/segmented-control/segmented-control.tsx b/packages/calcite-components/src/components/segmented-control/segmented-control.tsx index ab703ac4ff3..b455cb27b51 100644 --- a/packages/calcite-components/src/components/segmented-control/segmented-control.tsx +++ b/packages/calcite-components/src/components/segmented-control/segmented-control.tsx @@ -35,8 +35,10 @@ import { setComponentLoaded, setUpLoadableComponent, } from "../../utils/loadable"; -import { Appearance, Layout, Scale, Width } from "../interfaces"; +import { Appearance, Layout, Scale, Status, Width } from "../interfaces"; import { createObserver } from "../../utils/observers"; +import { Validation } from "../functional/Validation"; +import { CSS } from "./resources"; /** * @slot - A slot for adding `calcite-segmented-control-item`s. @@ -127,6 +129,15 @@ export class SegmentedControl } } + /** Specifies the status of the validation message. */ + @Prop({ reflect: true }) status: Status = "idle"; + + /** Specifies the validation message to display under the component. */ + @Prop() validationMessage: string; + + /** Specifies the validation icon to display under the component. */ + @Prop({ reflect: true }) validationIcon: string | boolean; + /** Specifies the width of the component. */ @Prop({ reflect: true }) width: Extract<"auto" | "full", Width> = "auto"; @@ -169,10 +180,20 @@ export class SegmentedControl render(): VNode { return ( - - - - +
+ + + + +
+ {this.validationMessage ? ( + + ) : null}
); } diff --git a/packages/calcite-components/src/demos/radio-button-group.html b/packages/calcite-components/src/demos/radio-button-group.html index 4e41b341938..3649df1315c 100644 --- a/packages/calcite-components/src/demos/radio-button-group.html +++ b/packages/calcite-components/src/demos/radio-button-group.html @@ -400,13 +400,90 @@ + +
+
Validation message displayed due to required property
+ +
+ + + + Stencil + + + + React + + + + Ember + + +
+ +
+ + + + Stencil + + + + React + + + + Ember + + +
+ +
+ + + + Stencil + + + + React + + + + Ember + + +
+
+
+ -->
Default
@@ -750,6 +827,83 @@
+ + +
+
Validation message displayed due to required property
+ +
+ + + + Stencil + + + + React + + + + Ember + + +
+ +
+ + + + Stencil + + + + React + + + + Ember + + +
+ +
+ + + + Stencil + + + + React + + + + Ember + + +
+
diff --git a/packages/calcite-components/src/demos/segmented-control.html b/packages/calcite-components/src/demos/segmented-control.html index 8ac3da36a67..6d28e1ef6c1 100644 --- a/packages/calcite-components/src/demos/segmented-control.html +++ b/packages/calcite-components/src/demos/segmented-control.html @@ -1452,6 +1452,114 @@
+ +
+
+
validation message - horizontal
+ +
+ + React + Ember + Angular + Vue + +
+ +
+ + React + Ember + Angular + Vue + +
+ +
+ + React + Ember + Angular + Vue + +
+
+ +
+
validation message - vertical
+ +
+ + React + Ember + Angular + Vue + +
+ +
+ + React + Ember + Angular + Vue + +
+ +
+ + React + Ember + Angular + Vue + +
+