diff --git a/git b/git new file mode 160000 index 0000000000..795ea8776b --- /dev/null +++ b/git @@ -0,0 +1 @@ +Subproject commit 795ea8776befc95ea2becd8020c7a284677b4161 diff --git a/schemas/1.6.0/adaptive-card.json b/schemas/1.6.0/adaptive-card.json index 1d4ffb949d..15e107f5b1 100644 --- a/schemas/1.6.0/adaptive-card.json +++ b/schemas/1.6.0/adaptive-card.json @@ -3269,6 +3269,11 @@ "description": "Error message to display when entered input is invalid", "version": "1.3" }, + "inputStyle": { + "type": "string", + "description": "input Style Hint", + "version": "1.6" + }, "isRequired": { "type": "boolean", "description": "Whether or not this input is required", diff --git a/schemas/src/elements/inputs/Input.json b/schemas/src/elements/inputs/Input.json index 89eaacb903..0e707cbfeb 100644 --- a/schemas/src/elements/inputs/Input.json +++ b/schemas/src/elements/inputs/Input.json @@ -15,6 +15,11 @@ "description": "Error message to display when entered input is invalid", "version": "1.3" }, + "inputStyle": { + "type": "string", + "description": "input Style Hint", + "version": "1.6" + }, "isRequired": { "type": "boolean", "description": "Whether or not this input is required", diff --git a/source/nodejs/ac-typed-schema/src/markdown/languages/de.json b/source/nodejs/ac-typed-schema/src/markdown/languages/de.json index d2b1fbea67..37059ddc02 100644 --- a/source/nodejs/ac-typed-schema/src/markdown/languages/de.json +++ b/source/nodejs/ac-typed-schema/src/markdown/languages/de.json @@ -232,5 +232,17 @@ "Style hint for `TableCell`.": "Style hint for `TableCell`.", "Allows users to filter choices in a choice set.": "Allows users to filter choices in a choice set.", "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) ot will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.": "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) ot will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.", - "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted." + "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.", + "Defines a source for captions": "Defines a source for captions", + "Defines various metadata properties": "Defines various metadata properties", + "Defines various metadata properties typically not used for rendering the card": "Defines various metadata properties typically not used for rendering the card", + "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) or will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.": "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) or will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.", + "Array of captions sources for the media element to provide.": "Array of captions sources for the media element to provide.", + "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, `mimeType` can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, `mimeType` can be omitted.", + "Mime type of associated caption file (e.g. `\"vtt\"`). For rendering in JavaScript, only `\"vtt\"` is supported, for rendering in UWP, `\"vtt\"` and `\"srt\"` are supported.": "Mime type of associated caption file (e.g. `\"vtt\"`). For rendering in JavaScript, only `\"vtt\"` is supported, for rendering in UWP, `\"vtt\"` and `\"srt\"` are supported.", + "URL to captions.": "URL to captions.", + "Label of this caption to show to the user.": "Label of this caption to show to the user.", + "A timestamp that informs a Host when the card content has expired, and that it should trigger a refresh as appropriate. The format is ISO-8601 Instant format. E.g., 2022-01-01T12:00:00Z": "A timestamp that informs a Host when the card content has expired, and that it should trigger a refresh as appropriate. The format is ISO-8601 Instant format. E.g., 2022-01-01T12:00:00Z", + "URL that uniquely identifies the card and serves as a browser fallback that can be used by some hosts.": "URL that uniquely identifies the card and serves as a browser fallback that can be used by some hosts.", + "input Style Hint": "input Style Hint" } \ No newline at end of file diff --git a/source/nodejs/ac-typed-schema/src/markdown/languages/en.json b/source/nodejs/ac-typed-schema/src/markdown/languages/en.json index d2b1fbea67..37059ddc02 100644 --- a/source/nodejs/ac-typed-schema/src/markdown/languages/en.json +++ b/source/nodejs/ac-typed-schema/src/markdown/languages/en.json @@ -232,5 +232,17 @@ "Style hint for `TableCell`.": "Style hint for `TableCell`.", "Allows users to filter choices in a choice set.": "Allows users to filter choices in a choice set.", "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) ot will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.": "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) ot will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.", - "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted." + "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.", + "Defines a source for captions": "Defines a source for captions", + "Defines various metadata properties": "Defines various metadata properties", + "Defines various metadata properties typically not used for rendering the card": "Defines various metadata properties typically not used for rendering the card", + "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) or will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.": "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) or will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.", + "Array of captions sources for the media element to provide.": "Array of captions sources for the media element to provide.", + "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, `mimeType` can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, `mimeType` can be omitted.", + "Mime type of associated caption file (e.g. `\"vtt\"`). For rendering in JavaScript, only `\"vtt\"` is supported, for rendering in UWP, `\"vtt\"` and `\"srt\"` are supported.": "Mime type of associated caption file (e.g. `\"vtt\"`). For rendering in JavaScript, only `\"vtt\"` is supported, for rendering in UWP, `\"vtt\"` and `\"srt\"` are supported.", + "URL to captions.": "URL to captions.", + "Label of this caption to show to the user.": "Label of this caption to show to the user.", + "A timestamp that informs a Host when the card content has expired, and that it should trigger a refresh as appropriate. The format is ISO-8601 Instant format. E.g., 2022-01-01T12:00:00Z": "A timestamp that informs a Host when the card content has expired, and that it should trigger a refresh as appropriate. The format is ISO-8601 Instant format. E.g., 2022-01-01T12:00:00Z", + "URL that uniquely identifies the card and serves as a browser fallback that can be used by some hosts.": "URL that uniquely identifies the card and serves as a browser fallback that can be used by some hosts.", + "input Style Hint": "input Style Hint" } \ No newline at end of file diff --git a/source/nodejs/ac-typed-schema/src/markdown/languages/sp.json b/source/nodejs/ac-typed-schema/src/markdown/languages/sp.json index d2b1fbea67..37059ddc02 100644 --- a/source/nodejs/ac-typed-schema/src/markdown/languages/sp.json +++ b/source/nodejs/ac-typed-schema/src/markdown/languages/sp.json @@ -232,5 +232,17 @@ "Style hint for `TableCell`.": "Style hint for `TableCell`.", "Allows users to filter choices in a choice set.": "Allows users to filter choices in a choice set.", "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) ot will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.": "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) ot will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.", - "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted." + "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.", + "Defines a source for captions": "Defines a source for captions", + "Defines various metadata properties": "Defines various metadata properties", + "Defines various metadata properties typically not used for rendering the card": "Defines various metadata properties typically not used for rendering the card", + "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) or will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.": "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) or will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.", + "Array of captions sources for the media element to provide.": "Array of captions sources for the media element to provide.", + "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, `mimeType` can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, `mimeType` can be omitted.", + "Mime type of associated caption file (e.g. `\"vtt\"`). For rendering in JavaScript, only `\"vtt\"` is supported, for rendering in UWP, `\"vtt\"` and `\"srt\"` are supported.": "Mime type of associated caption file (e.g. `\"vtt\"`). For rendering in JavaScript, only `\"vtt\"` is supported, for rendering in UWP, `\"vtt\"` and `\"srt\"` are supported.", + "URL to captions.": "URL to captions.", + "Label of this caption to show to the user.": "Label of this caption to show to the user.", + "A timestamp that informs a Host when the card content has expired, and that it should trigger a refresh as appropriate. The format is ISO-8601 Instant format. E.g., 2022-01-01T12:00:00Z": "A timestamp that informs a Host when the card content has expired, and that it should trigger a refresh as appropriate. The format is ISO-8601 Instant format. E.g., 2022-01-01T12:00:00Z", + "URL that uniquely identifies the card and serves as a browser fallback that can be used by some hosts.": "URL that uniquely identifies the card and serves as a browser fallback that can be used by some hosts.", + "input Style Hint": "input Style Hint" } \ No newline at end of file diff --git a/source/nodejs/adaptivecards-designer/src/adaptive-card-schema.ts b/source/nodejs/adaptivecards-designer/src/adaptive-card-schema.ts index 08e44ddefa..d304a4e767 100644 --- a/source/nodejs/adaptivecards-designer/src/adaptive-card-schema.ts +++ b/source/nodejs/adaptivecards-designer/src/adaptive-card-schema.ts @@ -693,7 +693,10 @@ export const adaptiveCardSchema = "value": { "type": "string", "description": "The initial value for a field" - } + }, + "inputStyle" : { + "$ref": "#/definitions/InputStyle" + } }, "required": [ "id" @@ -902,6 +905,14 @@ export const adaptiveCardSchema = "url", "email" ] - } + }, + "InputStyle": { + "type": "string", + "description": "Style hint for Input Fields.", + "enum": [ + "default", + "readWrite" + ] + } } }; diff --git a/source/nodejs/adaptivecards-designer/src/designer-peers.ts b/source/nodejs/adaptivecards-designer/src/designer-peers.ts index 87c52991c4..82583e6a5a 100644 --- a/source/nodejs/adaptivecards-designer/src/designer-peers.ts +++ b/source/nodejs/adaptivecards-designer/src/designer-peers.ts @@ -64,6 +64,7 @@ export class PropertySheetCategory { static readonly SelectionAction = "Selection action"; static readonly InlineAction = "Inline action"; static readonly Validation = "Validation"; + static readonly InputStyle = "Input Style"; static readonly Refresh = "Refresh" private _entries: PropertySheetEntry[] = []; @@ -2784,6 +2785,8 @@ export abstract class InputPeer extends TypedCard "errorMessage", "Error message"); + static readonly inputStyleProperty = new StringPropertyEditor(Adaptive.Versions.v1_6, "inputStyle", "Input Style"); + protected internalGetTreeItemText(): string { return this.cardElement.id ? this.cardElement.id : super.internalGetTreeItemText(); } @@ -2797,6 +2800,10 @@ export abstract class InputPeer extends TypedCard InputPeer.isRequiredProperty, InputPeer.errorMessageProperty); + propertySheet.add( + PropertySheetCategory.InputStyle, + InputPeer.inputStyleProperty); + propertySheet.remove( CardElementPeer.horizontalAlignmentProperty, CardElementPeer.heightProperty); diff --git a/source/nodejs/adaptivecards/example.html b/source/nodejs/adaptivecards/example.html index d94a2ad1c7..f41bfe1bfe 100644 --- a/source/nodejs/adaptivecards/example.html +++ b/source/nodejs/adaptivecards/example.html @@ -12,39 +12,65 @@ width: 250px; border: solid 1px black; } + textarea { + margin-top: 10px; + margin-left: 50px; + width: 500px; + height: 700px; + -moz-border-bottom-colors: none; + -moz-border-left-colors: none; + -moz-border-right-colors: none; + -moz-border-top-colors: none; + background: none repeat scroll 0 0 rgba(0, 0, 0, 0.07); + border-color: -moz-use-text-color #FFFFFF #FFFFFF -moz-use-text-color; + border-image: none; + border-radius: 6px 6px 6px 6px; + border-style: none solid solid none; + border-width: medium 1px 1px medium; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; + color: #555555; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 1em; + line-height: 1.4em; + padding: 5px 8px; + transition: background-color 0.2s ease 0s; + } + + + textarea:focus { + background: none repeat scroll 0 0 #FFFFFF; + outline-width: 0; + } + + .button { + background-color: #4CAF50; + border: none; + color: white; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + cursor: pointer; + } + + -

Adaptive Cards Example

- -

This example requires a build of the Adaptive Cards library.

- -

To run:

- -
$ npm install
-
$ npm run build
-
Refresh this page
-
- -

A card will render below

- -
+

Inline Edit Playground

+ +
+
+
+ + + + + + + +
+
+ +
+
+
+ +
+
+
diff --git a/source/nodejs/adaptivecards/package-lock.json b/source/nodejs/adaptivecards/package-lock.json index 3864d96f95..617ad1ca7a 100644 --- a/source/nodejs/adaptivecards/package-lock.json +++ b/source/nodejs/adaptivecards/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "adaptivecards", "version": "3.0.0-beta.2", "license": "MIT", "devDependencies": { diff --git a/source/nodejs/adaptivecards/src/card-elements.ts b/source/nodejs/adaptivecards/src/card-elements.ts index 5ad8cd4f9b..74310c1ba0 100644 --- a/source/nodejs/adaptivecards/src/card-elements.ts +++ b/source/nodejs/adaptivecards/src/card-elements.ts @@ -3092,6 +3092,26 @@ export abstract class Input extends CardElement implements IInput { static readonly labelProperty = new StringProperty(Versions.v1_3, "label", true); static readonly isRequiredProperty = new BoolProperty(Versions.v1_3, "isRequired", false); static readonly errorMessageProperty = new StringProperty(Versions.v1_3, "errorMessage", true); + static readonly inputStyleProperty = new EnumProperty( + Versions.v1_5, // TODO upgrade version + "inputStyle", + Enums.InputStyle, + Enums.InputStyle.Default, + [ + { value: Enums.InputStyle.Default }, + { value: Enums.InputStyle.ReadWrite } + ] + ); + static readonly labelAlignmentProperty = new EnumProperty( + Versions.v1_5, // TODO upgrade version + "labelAlignment", + Enums.InputLabelAlignment, + Enums.InputLabelAlignment.Vertical, + [ + { value: Enums.InputLabelAlignment.Horizontal }, + { value: Enums.InputLabelAlignment.Vertical } + ] + ); @property(Input.labelProperty) label?: string; @@ -3102,6 +3122,12 @@ export abstract class Input extends CardElement implements IInput { @property(Input.errorMessageProperty) errorMessage?: string; + @property(Input.inputStyleProperty) + inputStyle: Enums.InputStyle = Enums.InputStyle.Default; + + @property(Input.labelAlignmentProperty) + labelAlignment: Enums.InputLabelAlignment = Enums.InputLabelAlignment.Vertical; + //#endregion private _outerContainerElement: HTMLElement; @@ -3129,6 +3155,66 @@ export abstract class Input extends CardElement implements IInput { return labelIds; } + protected onUserEvents(inputElement?: HTMLElement, eventType?: string) { + if (!inputElement) { + return + } + if (this.inputStyle === Enums.InputStyle.ReadWrite) { + if (eventType === "onMouseEnter") { + // TODO use host color from hostConfig.inputs + inputElement.style.border = "1px solid #686868"; + } + if (eventType === "onMouseLeave") { + inputElement.style.border = "1px solid #E1E1E1"; + } + if (eventType === "onClick" || eventType === "onFocus") { + // TODO use host color from hostConfig.inputs + inputElement.style.border = "1px solid #5b5fc7"; + } + } + } + + protected handleMouseEvents(inputElement: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | undefined, eventType: string, source: string, type: string) { + if (!inputElement || this.inputStyle === null || this.inputStyle !== Enums.InputStyle.ReadWrite) { + return; + } + if (eventType == "onMouseEnter") { + if (source == "Card") { + const borderStyle = "1px solid #E1E1E1"; + inputElement.style.border = borderStyle; + if (type == "date" || type == "time") { + (inputElement as HTMLInputElement).readOnly = false; + inputElement.required = false; + } + if (type == "choiceSet") { + inputElement.style.appearance = "auto"; + } + } + if (source == "Self") { + const borderStyle = "1px solid #686868"; + inputElement.style.border = borderStyle; + } + } + + if (eventType == "onMouseLeave") { + if (source == "Card") { + if (type == "date" || type == "time") { + (inputElement as HTMLInputElement).readOnly = true; + inputElement.required = true; + } + if (type == "choiceSet") { + inputElement.style.appearance = "none"; + } + const borderStyle = "1px solid transparent"; + inputElement.style.border = borderStyle; + } + if (source == "Self") { + const borderStyle = "1px solid #E1E1E1"; + inputElement.style.border = borderStyle; + } + } + } + protected updateInputControlAriaLabelledBy() { if (this._renderedInputControlElement) { const labelIds: string[] = this.getAllLabelIds(); @@ -3161,8 +3247,8 @@ export abstract class Input extends CardElement implements IInput { this._outerContainerElement = document.createElement("div"); this._outerContainerElement.style.display = "flex"; - this._outerContainerElement.style.flexDirection = "column"; - + this._outerContainerElement.style.flexDirection = "column"; + const renderedInputControlId = Utils.generateUniqueId(); if (this.label) { @@ -3191,9 +3277,13 @@ export abstract class Input extends CardElement implements IInput { if (this._renderedLabelElement) { this._renderedLabelElement.id = Utils.generateUniqueId(); - this._renderedLabelElement.style.marginBottom = - hostConfig.getEffectiveSpacing(hostConfig.inputs.label.inputSpacing) + "px"; - + if (this.labelAlignment === Enums.InputLabelAlignment.Horizontal) { + // horizontal alignment, label should be in center of the div and no extra spacing needed + this._renderedLabelElement.style.alignSelf = "center"; + } else { + this._renderedLabelElement.style.marginBottom = + hostConfig.getEffectiveSpacing(hostConfig.inputs.label.inputSpacing) + "px"; + } this._outerContainerElement.appendChild(this._renderedLabelElement); } } @@ -3224,11 +3314,24 @@ export abstract class Input extends CardElement implements IInput { this._inputControlContainerElement.appendChild(this._renderedInputControlElement); this._outerContainerElement.appendChild(this._inputControlContainerElement); + if (this._renderedLabelElement && this.labelAlignment === Enums.InputLabelAlignment.Horizontal) { + this._outerContainerElement.style.flexDirection = "row"; + this._renderedLabelElement.style.width = "30%"; + this._inputControlContainerElement.style.width = "70%"; + } + + if (this.inputStyle === Enums.InputStyle.ReadWrite) { + this._outerContainerElement.classList.add( + this.hostConfig.makeCssClassName("ac-input-outer-container-readWrite") + ); + } this.updateInputControlAriaLabelledBy(); return this._outerContainerElement; } + + this.resetDirtyState(); return undefined; @@ -3442,6 +3545,9 @@ export class TextInput extends Input { input.maxLength = this.maxLength; } + if (this.inputStyle !== null && this.inputStyle === Enums.InputStyle.ReadWrite) { + input.style.background = "transparent"; + } input.oninput = () => { this.valueChanged(); }; @@ -3478,6 +3584,27 @@ export class TextInput extends Input { result.type = Enums.InputTextStyle[this.style].toLowerCase(); } + if (this.inputStyle !== null && this.inputStyle === Enums.InputStyle.ReadWrite) { + result.style.background = "transparent"; + result.style.border = "1px solid transparent"; + } + + (this._parent as AdaptiveCard).registerMouseEnterCallback( + (ev: MouseEvent) => this.handleMouseEvents(result, "onMouseEnter", "Card", "text") + ); + + (this._parent as AdaptiveCard).registerMouseLeaveCallback( + (ev: MouseEvent) => this.handleMouseEvents(result, "onMouseLeave", "Card", "text") + ); + + result.onmouseenter = (ev: MouseEvent) => { + this.handleMouseEvents(result, "onMouseEnter", "Self", "text"); + }; + + result.onmouseleave = (ev: MouseEvent) => { + this.handleMouseEvents(result, "onMouseLeave", "Self", "text"); + }; + this.setupInput(result); return result; @@ -3858,6 +3985,7 @@ export class ChoiceSetInput extends Input { private _uniqueCategoryName: string; private _selectElement: HTMLSelectElement | undefined; + private _inputElement: HTMLInputElement | undefined; private _textInput: HTMLInputElement | undefined; private _toggleInputs: HTMLInputElement[] | undefined; private _labels: Array; @@ -4123,10 +4251,34 @@ export class ChoiceSetInput extends Input { this.valueChanged(); }; + if (this.inputStyle !== null && this.inputStyle === Enums.InputStyle.ReadWrite) { + this._selectElement.style.background = "transparent"; + this._selectElement.style.border = "1px solid transparent"; + this._selectElement.style.appearance = "none"; + } + + this.internalApplyAriaCurrent(); - return this._selectElement; + (this._parent as AdaptiveCard).registerMouseEnterCallback( + (ev: MouseEvent) => this.handleMouseEvents(this._selectElement, "onMouseEnter", "Card", "choiceSet") + ); + + (this._parent as AdaptiveCard).registerMouseLeaveCallback( + (ev: MouseEvent) => this.handleMouseEvents(this._selectElement, "onMouseLeave", "Card", "choiceSet") + ); + + this._selectElement.onmouseenter = (ev: MouseEvent) => { + this.handleMouseEvents(this._selectElement, "onMouseEnter", "Self", "choiceSet"); + }; + + this._selectElement.onmouseleave = (ev: MouseEvent) => { + this.handleMouseEvents(this._selectElement, "onMouseLeave", "Self", "choiceSet"); + }; } + + return this._selectElement; + } } @@ -4372,20 +4524,41 @@ export class DateInput extends Input { } this._dateInputElement.tabIndex = this.isDesignMode() ? -1 : 0; - this._dateInputElement.className = this.hostConfig.makeCssClassName( "ac-input", "ac-dateInput" ); + if (this.inputStyle !== null && this.inputStyle === Enums.InputStyle.ReadWrite) { + this._dateInputElement.style.background = "transparent"; + this._dateInputElement.style.border = "1px solid transparent"; + this._dateInputElement.readOnly = true; + this._dateInputElement.required = true; + } + + if (this.defaultValue) { + this._dateInputElement.value = this.defaultValue; + } + this._dateInputElement.style.width = "100%"; this._dateInputElement.oninput = () => { this.valueChanged(); }; + (this._parent as AdaptiveCard).registerMouseEnterCallback( + (ev: MouseEvent) => this.handleMouseEvents(this._dateInputElement, "onMouseEnter", "Card", "date") + ); - if (this.defaultValue) { - this._dateInputElement.value = this.defaultValue; - } + (this._parent as AdaptiveCard).registerMouseLeaveCallback( + (ev: MouseEvent) => this.handleMouseEvents(this._dateInputElement, "onMouseLeave", "Card", "date") + ); + + this._dateInputElement.onmouseenter = (ev: MouseEvent) => { + this.handleMouseEvents(this._dateInputElement, "onMouseEnter", "Self", "date"); + }; + + this._dateInputElement.onmouseleave = (ev: MouseEvent) => { + this.handleMouseEvents(this._dateInputElement, "onMouseLeave", "Self", "date"); + }; return this._dateInputElement; } @@ -4487,6 +4660,15 @@ export class TimeInput extends Input { private _timeInputElement: HTMLInputElement; + protected applyReadOnlyStyling(inputElement: HTMLInputElement): HTMLInputElement { + // css styling + inputElement.style.background = "transparent"; + inputElement.style.border = "1px solid transparent"; + + inputElement.readOnly = true; + return inputElement; + } + protected internalRender(): HTMLElement | undefined { this._timeInputElement = document.createElement("input"); this._timeInputElement.setAttribute("type", "time"); @@ -4519,6 +4701,29 @@ export class TimeInput extends Input { this._timeInputElement.value = this.defaultValue; } + if (this.inputStyle !== null && this.inputStyle === Enums.InputStyle.ReadWrite) { + this._timeInputElement.style.background = "transparent"; + this._timeInputElement.style.border = "1px solid transparent"; + this._timeInputElement.readOnly = true; + this._timeInputElement.required = true; + } + + (this._parent as AdaptiveCard).registerMouseEnterCallback( + (ev: MouseEvent) => this.handleMouseEvents(this._timeInputElement, "onMouseEnter", "Card", "time") + ); + + (this._parent as AdaptiveCard).registerMouseLeaveCallback( + (ev: MouseEvent) => this.handleMouseEvents(this._timeInputElement, "onMouseLeave", "Card", "time") + ); + + this._timeInputElement.onmouseenter = (ev: MouseEvent) => { + this.handleMouseEvents(this._timeInputElement, "onMouseEnter", "Self", "time"); + }; + + this._timeInputElement.onmouseleave = (ev: MouseEvent) => { + this.handleMouseEvents(this._timeInputElement, "onMouseLeave", "Self", "time"); + }; + return this._timeInputElement; } @@ -4997,7 +5202,7 @@ export abstract class SubmitActionBase extends Action { context.serializeValue(target, prop.name, value); } ); - static readonly disabledUnlessAssociatedInputsChangeProperty = new BoolProperty(Versions.v1_6, "disabledUnlessAssociatedInputsChange", false); + static readonly disabledUnlessAssociatedInputsChangeProperty = new BoolProperty(Versions.v1_5, "disabledUnlessAssociatedInputsChange", false); @property(SubmitActionBase.dataProperty) private _originalData?: PropertyBag; @@ -8253,6 +8458,18 @@ export class AdaptiveCard extends ContainerWithActions { private _fallbackCard?: AdaptiveCard; + private _mouseEnterCallbacks: any[] = []; + + private _mouseLeaveCallbacks: any[] = []; + + public registerMouseEnterCallback(callback: any) { + this._mouseEnterCallbacks.push(callback); + } + + public registerMouseLeaveCallback(callback: any) { + this._mouseLeaveCallbacks.push(callback); + } + private isVersionSupported(): boolean { if (this.bypassVersionCheck) { return true; @@ -8350,6 +8567,24 @@ export class AdaptiveCard extends ContainerWithActions { return true; } + onMouseEvent(renderedCard?: HTMLElement, eventType?: string) { + const inputElementsWithReadWriteStyleClass = renderedCard?.getElementsByClassName('ac-input-outer-container-readWrite'); + for (const inputContainer of Array.from(inputElementsWithReadWriteStyleClass || [])) { + const inputNode = inputContainer.getElementsByTagName('input'); + if (inputNode && inputNode[0]) { + // border width and color decided by the host + let borderStyle = ""; + if (eventType === "onMouseEnter") { + borderStyle = "1px solid #E1E1E1"; + } + if (eventType === "onMouseLeave") { + borderStyle = ""; + } + (inputNode[0] as HTMLElement).style.border = borderStyle; + } + } + } + onAnchorClicked?: (element: CardElement, anchor: HTMLAnchorElement, ev?: MouseEvent) => boolean; onExecuteAction?: (action: Action) => void; onElementVisibilityChanged?: (element: CardElement) => void; @@ -8420,6 +8655,15 @@ export class AdaptiveCard extends ContainerWithActions { if (this.speak) { renderedCard.setAttribute("aria-label", this.speak); } + renderedCard.onmouseenter = (ev: MouseEvent) => { + //this.onMouseEvent(renderedCard, "onMouseEnter"); + this._mouseEnterCallbacks.forEach(callback => callback(ev)); + }; + + renderedCard.onmouseleave = (ev: MouseEvent) => { + //this.onMouseEvent(renderedCard, "onMouseLeave"); + this._mouseLeaveCallbacks.forEach(callback => callback(ev)); + }; } } diff --git a/source/nodejs/adaptivecards/src/enums.ts b/source/nodejs/adaptivecards/src/enums.ts index 5a84ec6067..a6f6287f85 100644 --- a/source/nodejs/adaptivecards/src/enums.ts +++ b/source/nodejs/adaptivecards/src/enums.ts @@ -152,6 +152,16 @@ export enum InputTextStyle { Password } +export enum InputStyle { + Default, + ReadWrite +} + +export enum InputLabelAlignment { + Horizontal, + Vertical +} + export enum ValidationPhase { Parse, ToJSON, diff --git a/source/nodejs/adaptivecards/src/scss/adaptivecards-base.scss b/source/nodejs/adaptivecards/src/scss/adaptivecards-base.scss index 3b3ba5ddc3..3b2dccc627 100644 --- a/source/nodejs/adaptivecards/src/scss/adaptivecards-base.scss +++ b/source/nodejs/adaptivecards/src/scss/adaptivecards-base.scss @@ -303,3 +303,53 @@ $font-family: 'Segoe UI', sans-serif; } } +@mixin base-inputRW ( + $font-size: 14px, + $padding: 4px 8px 4px 8px, + $color: black, + $border: 1px solid #DDDDDD, + $hover-border: null, + $background-color: null, + $hover-background-color: null, + $validation-failed-border: 1px solid red !important, + $validation-failed-outline: 1px solid red + ) { + + .ac-inputRW { + font-family: $font-family; + font-size: $font-size; + color: $color; + border: 1px solid transparent;; + &.ac-textInputRW { + resize: none; + } + + &.ac-textInputRW.ac-multilineRW { + height: 72px; + } + + &.ac-textInputRW, &.ac-numberInputRW, &.ac-dateInputRW, &.ac-timeInputRW, &.ac-multichoiceInputRW { + background-color: $background-color; + + &:hover { + outline: auto; + } + } + + &.ac-textInputRW, + &.ac-numberInputRW, + &.ac-dateInputRW, + &.ac-timeInputRW, + &.ac-multichoiceInputRW.ac-choiceSetInput-compactRW { + &.ac-input-validation-failedRW { + border: $validation-failed-border; + } + } + + &.ac-toggleInputRW, &.ac-choiceSetInput-expandedRW, &.ac-choiceSetInput-multiSelectRW { + &.ac-input-validation-failedRW { + outline: $validation-failed-outline; + } + } + } +} diff --git a/source/nodejs/adaptivecards/src/scss/adaptivecards-default.scss b/source/nodejs/adaptivecards/src/scss/adaptivecards-default.scss index 15eb91b9bb..2386827111 100644 --- a/source/nodejs/adaptivecards/src/scss/adaptivecards-default.scss +++ b/source/nodejs/adaptivecards/src/scss/adaptivecards-default.scss @@ -46,7 +46,7 @@ $destructive-hl-bg-color: #BF0000; ); @include base-input; - +@include base-inputRW; /* ac-inlineActionButton should set height to the same as ac-input.ac-textInput */ .ac-inlineActionButton { diff --git a/source/nodejs/package.json b/source/nodejs/package.json index dc8b8a97ec..e85ed9fa11 100644 --- a/source/nodejs/package.json +++ b/source/nodejs/package.json @@ -41,14 +41,14 @@ "rimraf": "^3.0.2", "sass": "^1.43.4", "style-loader": "^3.3.1", + "svg-url-loader": "^7.1.1", "ts-jest": "^27.0.7", "ts-loader": "^9.2.6", "typescript": "^4.4.4", "webpack": "^5.60.0", "webpack-cli": "^4.9.1", "webpack-concat-files-plugin": "^0.5.2", - "webpack-dev-server": "^4.3.1", - "svg-url-loader": "^7.1.1" + "webpack-dev-server": "^4.3.1" }, "clang-format-launcher": { "includeEndsWith": [ diff --git a/specs/DesignDiscussions/inlineEditability.md b/specs/DesignDiscussions/inlineEditability.md new file mode 100644 index 0000000000..5b9ff30208 --- /dev/null +++ b/specs/DesignDiscussions/inlineEditability.md @@ -0,0 +1,241 @@ +# Inline Editability for Adaptive Cards + +## Overview + +Adaptive cards are used by apps to share information as well as to collect input from users to complete user scenarios. +As of now, if we are showing some information in a card and we also want to collect user’s input, we will have bunch of Input fields hidden behind a ShowCard button or we will launch some form using button click on the card to collect user's input. +In both cases we might have duplicate readable fields and input fields. + +We want to provide better experience to the user where he can read the fields and can also seamlessly update the values at the same place. + +UI of showing data and taking user input will become simplistic. + + +## Current experience: + +As shown in the below picture, An adaptive card displays data for Customer name, Est. Revenue and Est. Clode Date. If user wants to update those information, he will have to click on 'Edit' button which will open some form and there he can update values for 'Est. Revenue' etc. and Click on Save to send data back to the bot service. + +![img](../assets/InlineEditability/IE_1.PNG) + + +As of now, Adaptive cards support `Input` fields to collect user input. This is how current user experience of `Input` fields looks like. + + ![img](../assets/InlineEditability/IE18.PNG) + +## Proposed experience: + +We will allow bot developer to enhance the user experience of all `Input` fields in Adaptive card (such as `Input.Text`, `Input.Number`, `Input.Date`, `Input.Time`, `Input.Toggle` and `Input.Choiceset`) in way that, these input fields can appear just as readable fields when user is not taking any action and when user clicks or focusses or clicks on them, it allows user to update those fields and then user can use action buttons like Action.Submit/Action.Execute to send data back to the bot. + +This is the flow for user to interact with inline editable fields:- +1. This is an adaptive showing information like "Contact", "Email", "Est. Closing date" etc. with inline Editable style where input fields are displayed as **readable fields** in the **default** state. + +![img](../assets/InlineEditability/IE1.PNG) + +2. When user hovers on the card, + * we will show outer outline on that card. + * we will show a lighter color outline to all the input fields which are inline editable. + + This will guide user to the input fields which he can go an update right on the card. + +![img](../assets/InlineEditability/IE2.PNG) + +3. When user takes hovers over the input field, we will show a darker outline in that input field to guide user that the field is activated and he can now edit the field. + +![img](../assets/InlineEditability/IE15.PNG) + +4. User can go and edit the field. + +![img](../assets/InlineEditability/IE3.PNG) + +5. When user has updated the input field, Action.Submit or Action.Execute button such as "Save" which has `disabledUnlessAssociatedInputsChange` property (https://github.com/microsoft/AdaptiveCards/issues/7103) will get enabled automatically. It will guide user to hit the button to commit the changes made on the card. + +![img](../assets/InlineEditability/IE4.PNG) + + +6. When user clicks hits on the Action.Submit or Action.Execute button, latest input values will be sent to the bot from host and bot can then update the card with latest information which we will show in default state. + +![img](../assets/InlineEditability/IE5.PNG) + +## New Capabilites in the card +In order to achieve inline editable experience as mentioned above, we will add these capabilities in the card for developers and host: + +## New Schema Changes in Adaptive Card +1. Introduce an optional styling property in AC input fields schema for developer to choose between existing input fields styling vs inline editable styling. + +* **Existing view:** + + ![img](../assets/InlineEditability/IE18.PNG) + +* **Inline Editable View:** + +![img](../assets/InlineEditability/IE7.PNG) + +2. Introduce an optional new styling property in AC input fields schema which will allow developers to define position of label with respect to input field. It can be both "inline" and "above". Default would be existing "above". This feature will be independent of whether input fields are inline editable or not. We will allow horizontal view for default input fields as well. + *Note*: When card width is small due to browser resize etc, horizontal view can shift to vertical view of input fields. + +* **Above view:** + +![img](../assets/InlineEditability/IE6.PNG) + +* **Inline view:** + +![img](../assets/InlineEditability/IE7.PNG) + +See below section for proposed schema changes (https://github.com/baton17/AdaptiveCards/blob/inLineEditability/specs/DesignDiscussions/inlineEditability.md#proposed-schema-changes) + +## Host configurable properties in card: +1. Host can configure styling property to define width percentage of label and value properties for input elements. +In horizontal view, by default, label:value width is to be 3:7 of the container of input element. however, host can configure it for themselves. + +![img](../assets/InlineEditability/IE9.PNG) + +In vertical view, the width is always 100% of the container of input element. + +![img](../assets/InlineEditability/IE8.PNG) + + +2. Host can configure these style properties of `label` and `value` field: `font`, `font color` and `font weight`. + Default (for both label and value): `14px; Seogue Regular`. + Colour may be changed from the set of accessible colours in Accessible Messages Design System_WIP + +![img](../assets/InlineEditability/IE17.PNG) + +## Use Case Clarifications: + +1. If `label` is `empty` in the input field then value takes 100% width of the container of the input element. + +2. If `value` is `empty` in the input field. We will show label and placeholder text in lighter color like this in the [default] state, + +![img](../assets/InlineEditability/IE12.PNG) + +which user can go and update the value, + +![img](../assets/InlineEditability/IE13.PNG) + +3. If `label` is `empty`, we will just show `value` field in readable format in the default state which user can go and update the value. + +![img](../assets/InlineEditability/IE14.PNG) + +4. Order of `label` and `value` will always be label first and value second. In RTL, it should follow right to left convention as expected. + +![img](../assets/InlineEditability/IE10.PNG) + +5. If user has some unsaved changes on the card but he moved away from the card but card is still in the viewport. In this case, we will preseve the changes made by the user and we will some indication to user that fields are unsaved or dirty. When card goes out of the viewport, then we wont preserve the unsaved values. + +## Developer Recommendation: + +1. **Save Button:** Our recommendation is to have a `Save` button (Action.Submit or Action.Execute) with `disabledUnlessAssociatedInputsChange` on the card with inline editable fields. Save button will send the modified user input values to the bot and bot will respond with a card with updated input values. + +2. **Cancel Button:** Our recommendation is to have a `Cancel` button (Action.Submit or Action.Execute) on the card with inline editable fields. If user has made some changes in the input fields but want to revert back to original state, they can click on "Cancel" button. "Cancel" button will send the card with last saved input values. +## Out of Scope: + +1. Inline Action in Input.text: We will not support inline Editable styles for input text containing inline action. It will be shown as default input style only. Reason being, such sceanrios are reply with a comment or adding a message where user collaboration is not needed. + +![img](../assets/InlineEditability/InlineEditabilitySecnario8.png) + +2. Mobile is `out of scope` for inline editable design since there is no focus state on mobile and alignment will be always `vertical` as of today. + +## Proposed Schema Changes: + + 1. Inherited properties of all Input fields will have one more property called `inputStyle` whose type will be `InputStyle` + +| Property | Type | Required | Description | Version | +| -------- | ---- | -------- | ----------- | ------- | +| **inputStyle** | `InputStyle?` | No | style hint for Input fields | 1.7 | + + +### inputStyle + +Style hint for input fields. + +* **Type**: `InputStyle` +* **Required**: No +* **Allowed values**: + * `"readWrite"` : Should show input fields as inline editable field, which is showing them in readable view unless user takes any action. + +Since this property is inherited to all Input fields, it will be supported by all: + +* `Input.Text` +* `Input.Number` +* `Input.Date` +* `Input.Time` +* `Input.Toggle` +* `Input.Choiceset` + +### Sample Payload: + +```json + { + "type": "Input.Text", + "label": "Name", + "value": "Sneh", + "inputStyle" : "readWrite" + }, + { + "type": "Input.Time", + "label": "Time of Arrival", + "value": "09:30", + "inputStyle" : "readWrite" + }, + { + "type": "Input.Number", + "label": "Number of Guest", + "value": 5, + "inputStyle" : "readWrite" + } +``` + +2. Inherited properties of all Input fields will have one more property called `labelPosition` whose type will be `InputLabelPosition` + +| Property | Type | Required | Description | Version | +| -------- | ---- | -------- | ----------- | ------- | +| **labelPosition** | `InputLabelPosition?` | No | Determines the position of the label with respect to the input field. Default is "above" when not specified | 1.7 | + +### InputLabelPosition + +Position for label in input fields. + +* **Type**: `InputLabelPosition` +* **Required**: No +* **Allowed values**: + * `"inline"` : should place label inline with the input field + * `"above"` : should place label above the input field. + +### Sample Payload: + +```json + { + "type": "Input.Text", + "label": "Name", + "value": "Sneh", + "inputStyle" : "readWrite", + "labelPosition" : "above" + }, + { + "type": "Input.Time", + "label": "Time of Arrival", + "value": "09:30", + "inputStyle" : "readWrite", + "labelPosition" : "inline" + }, + { + "type": "Input.Number", + "label": "Number of Guest", + "value": 5, + "labelPosition" : "inline" + }, + { + "type": "Input.Text", + "label": "Flight origin", + "value": "Seattle", + "labelPosition" : "above" + }, + { + "type": "Input.Text", + "label": "Flight destination", + "value": "Hyderabad", + } +``` + + + diff --git a/specs/assets/InlineEditability/IE1.PNG b/specs/assets/InlineEditability/IE1.PNG new file mode 100644 index 0000000000..900bafaeb6 Binary files /dev/null and b/specs/assets/InlineEditability/IE1.PNG differ diff --git a/specs/assets/InlineEditability/IE10.PNG b/specs/assets/InlineEditability/IE10.PNG new file mode 100644 index 0000000000..ae65f7a9a0 Binary files /dev/null and b/specs/assets/InlineEditability/IE10.PNG differ diff --git a/specs/assets/InlineEditability/IE11.PNG b/specs/assets/InlineEditability/IE11.PNG new file mode 100644 index 0000000000..83772b7e29 Binary files /dev/null and b/specs/assets/InlineEditability/IE11.PNG differ diff --git a/specs/assets/InlineEditability/IE12.PNG b/specs/assets/InlineEditability/IE12.PNG new file mode 100644 index 0000000000..9e19831393 Binary files /dev/null and b/specs/assets/InlineEditability/IE12.PNG differ diff --git a/specs/assets/InlineEditability/IE13.PNG b/specs/assets/InlineEditability/IE13.PNG new file mode 100644 index 0000000000..41b71bbc12 Binary files /dev/null and b/specs/assets/InlineEditability/IE13.PNG differ diff --git a/specs/assets/InlineEditability/IE14.PNG b/specs/assets/InlineEditability/IE14.PNG new file mode 100644 index 0000000000..881e4000ed Binary files /dev/null and b/specs/assets/InlineEditability/IE14.PNG differ diff --git a/specs/assets/InlineEditability/IE15.PNG b/specs/assets/InlineEditability/IE15.PNG new file mode 100644 index 0000000000..c0d11aefff Binary files /dev/null and b/specs/assets/InlineEditability/IE15.PNG differ diff --git a/specs/assets/InlineEditability/IE16.png b/specs/assets/InlineEditability/IE16.png new file mode 100644 index 0000000000..1f5ee75b59 Binary files /dev/null and b/specs/assets/InlineEditability/IE16.png differ diff --git a/specs/assets/InlineEditability/IE17.PNG b/specs/assets/InlineEditability/IE17.PNG new file mode 100644 index 0000000000..8aa3b167e0 Binary files /dev/null and b/specs/assets/InlineEditability/IE17.PNG differ diff --git a/specs/assets/InlineEditability/IE18.PNG b/specs/assets/InlineEditability/IE18.PNG new file mode 100644 index 0000000000..6d30c31dfb Binary files /dev/null and b/specs/assets/InlineEditability/IE18.PNG differ diff --git a/specs/assets/InlineEditability/IE2.PNG b/specs/assets/InlineEditability/IE2.PNG new file mode 100644 index 0000000000..9d652ac694 Binary files /dev/null and b/specs/assets/InlineEditability/IE2.PNG differ diff --git a/specs/assets/InlineEditability/IE3.PNG b/specs/assets/InlineEditability/IE3.PNG new file mode 100644 index 0000000000..84d9f53de7 Binary files /dev/null and b/specs/assets/InlineEditability/IE3.PNG differ diff --git a/specs/assets/InlineEditability/IE4.PNG b/specs/assets/InlineEditability/IE4.PNG new file mode 100644 index 0000000000..b1fc11257f Binary files /dev/null and b/specs/assets/InlineEditability/IE4.PNG differ diff --git a/specs/assets/InlineEditability/IE5.PNG b/specs/assets/InlineEditability/IE5.PNG new file mode 100644 index 0000000000..e90558c874 Binary files /dev/null and b/specs/assets/InlineEditability/IE5.PNG differ diff --git a/specs/assets/InlineEditability/IE6.PNG b/specs/assets/InlineEditability/IE6.PNG new file mode 100644 index 0000000000..77cf245250 Binary files /dev/null and b/specs/assets/InlineEditability/IE6.PNG differ diff --git a/specs/assets/InlineEditability/IE7.PNG b/specs/assets/InlineEditability/IE7.PNG new file mode 100644 index 0000000000..b8b32609f5 Binary files /dev/null and b/specs/assets/InlineEditability/IE7.PNG differ diff --git a/specs/assets/InlineEditability/IE8.PNG b/specs/assets/InlineEditability/IE8.PNG new file mode 100644 index 0000000000..fdab18de4a Binary files /dev/null and b/specs/assets/InlineEditability/IE8.PNG differ diff --git a/specs/assets/InlineEditability/IE9.PNG b/specs/assets/InlineEditability/IE9.PNG new file mode 100644 index 0000000000..c98bd8e5ea Binary files /dev/null and b/specs/assets/InlineEditability/IE9.PNG differ diff --git a/specs/assets/InlineEditability/IE_1.PNG b/specs/assets/InlineEditability/IE_1.PNG new file mode 100644 index 0000000000..b2dfc18c8a Binary files /dev/null and b/specs/assets/InlineEditability/IE_1.PNG differ diff --git a/specs/assets/InlineEditability/InlineEditabilitySecnario1.png b/specs/assets/InlineEditability/InlineEditabilitySecnario1.png new file mode 100644 index 0000000000..0f7314632d Binary files /dev/null and b/specs/assets/InlineEditability/InlineEditabilitySecnario1.png differ diff --git a/specs/assets/InlineEditability/InlineEditabilitySecnario2.png b/specs/assets/InlineEditability/InlineEditabilitySecnario2.png new file mode 100644 index 0000000000..f377509626 Binary files /dev/null and b/specs/assets/InlineEditability/InlineEditabilitySecnario2.png differ diff --git a/specs/assets/InlineEditability/InlineEditabilitySecnario3.png b/specs/assets/InlineEditability/InlineEditabilitySecnario3.png new file mode 100644 index 0000000000..5997fdc085 Binary files /dev/null and b/specs/assets/InlineEditability/InlineEditabilitySecnario3.png differ diff --git a/specs/assets/InlineEditability/InlineEditabilitySecnario4.png b/specs/assets/InlineEditability/InlineEditabilitySecnario4.png new file mode 100644 index 0000000000..5c387eaefe Binary files /dev/null and b/specs/assets/InlineEditability/InlineEditabilitySecnario4.png differ diff --git a/specs/assets/InlineEditability/InlineEditabilitySecnario5.png b/specs/assets/InlineEditability/InlineEditabilitySecnario5.png new file mode 100644 index 0000000000..1d1d1f0d13 Binary files /dev/null and b/specs/assets/InlineEditability/InlineEditabilitySecnario5.png differ diff --git a/specs/assets/InlineEditability/InlineEditabilitySecnario6.png b/specs/assets/InlineEditability/InlineEditabilitySecnario6.png new file mode 100644 index 0000000000..0e0a93deeb Binary files /dev/null and b/specs/assets/InlineEditability/InlineEditabilitySecnario6.png differ diff --git a/specs/assets/InlineEditability/InlineEditabilitySecnario7.png b/specs/assets/InlineEditability/InlineEditabilitySecnario7.png new file mode 100644 index 0000000000..a52bbaeaf1 Binary files /dev/null and b/specs/assets/InlineEditability/InlineEditabilitySecnario7.png differ diff --git a/specs/assets/InlineEditability/InlineEditabilitySecnario8.png b/specs/assets/InlineEditability/InlineEditabilitySecnario8.png new file mode 100644 index 0000000000..4a38198e1a Binary files /dev/null and b/specs/assets/InlineEditability/InlineEditabilitySecnario8.png differ diff --git a/specs/features/InlineEditability.md b/specs/features/InlineEditability.md new file mode 100644 index 0000000000..0751efdb6b --- /dev/null +++ b/specs/features/InlineEditability.md @@ -0,0 +1,147 @@ +# Inline Editability for Adaptive Cards + +# Overview + +Adaptive cards are used by apps to share information as well as to collect input from users to complete user scenarios. +As of now, if we are showing some information in a card and we also want to collect user’s input, we will have bunch of Input fields hidden behind a ShowCard button or we will launch some form using button click on the card to collect user's input. +In both cases we might have duplicate readable fields and input fields. + +We want to provide better experience to the user where he can read the fields and can also update the values at the same place. + +UI of showing data and taking user input will become simplistic. + + +## Current experience: + +As shown in the below picture, An adaptive card displays data for Customer name, Est. Revenue and Est. Clode Date. If an user wants to update those information, he will have to click on 'Edit' button which will open some form and there he can update values for 'Est. Revenue' etc. and Click on Save to send data back to the bot service. + +![img](../assets/InlineEditability/InlineEditabilitySecnario1.png) + + + +## Proposed experience: +Adaptive cards use `Input` fields to collect user data. This is how current user experience of `Input` fields looks like: + + ![img](../assets/InlineEditability/InlineEditabilitySecnario7.png) + + +We will allow bot developer to enhance the user experience of all `Input` fields in Adaptive card (such as `Input.Text`, `Input.Number`, `Input.Date`, `Input.Time`, `Input.Toggle` and `Input.Choiceset`) in way that, these input fields can appear just as readable fields when user is not taking any action and when user clicks or focusses on them, it allows user to update those fields and then user can use use action buttons like Action.Submit/Action.Execute to send data back to the bot. + + +This is an adaptive showing information like "Status", "Owner", "Est. Revenue" and "Est. Close Date" +![img](../assets/InlineEditability/InlineEditabilitySecnario2.png) + +When user want to update lets say "Est. Revenue", he can click on the value and card will allow user to update the value. and after updating he can click on "Save" button to send data back to the bot. + +![img](../assets/InlineEditability/InlineEditabilitySecnario3.png) + +To achieve this, `Input` fields will have `readWrite` as their input style property, which will modify the user experience as follows: (In the above picture "Est. Revenue" is of type `Input.Text`) + +* `label` and `value` property of Input field will be aligned in horizontal sections so that it looks like key-value pair of readable information. +* when user is not taking any action, Input field will look just like readable fields diplaying 'label' and 'value' without any edit field properties like border or background highlight on the 'value' section. +* when user clicks or focusses on the 'value' section, card will allow user to edit the 'value' section and it will show edit field properties like like border or background highlight on the 'value' section. + +Similar user experience for `Input.Date` with 'readWrite' inputStyle property. + +![img](../assets/InlineEditability/InlineEditabilitySecnario4.png) + +and `Input.Choiceset` with 'readWrite' inputStyle property. + +![img](../assets/InlineEditability/InlineEditabilitySecnario6.png) + +Other 'Input' fields like `Input.Number`, `Input.Time` and `Input.Toggle` will also follow the similar pattern for 'readWrite' inputStyle property. + +TBD: Update pictures with 'readWrite' user experience for all Input fields. + +## Schema Changes: + + Inherited properties of all Input fields will have one more property called `inputStyle` whose type will be `InputStyle` + +| Property | Type | Required | Description | Version | +| -------- | ---- | -------- | ----------- | ------- | +| **inputStyle** | `InputStyle` | No | style hint for Input fields | 1.3 | + + +### inputStyle + +Style hint for input fields. + +* **Type**: `InputStyle` +* **Required**: No +* **Allowed values**: + * `"readWrite"` : Should show as readable field unless user clicks or focusses on it. Label and Value will be horizontally aligned. + * `"default"` : Default behavior. + +Since this property is inherited to all Input fields, it will be supported by all: + +* `Input.Text` +* `Input.Number` +* `Input.Date` +* `Input.Time` +* `Input.Toggle` +* `Input.Choiceset` + +## Sample Payload: + +```json + { + "type": "Input.Text", + "label": "Name", + "value": "Sneh", + "inputStyle" : "readWrite" + }, + { + "type": "Input.Time", + "label": "Time of Arrival", + "value": "09:30", + "inputStyle" : "readWrite" + }, + { + "type": "Input.Number", + "label": "Number of Guest", + "value": 5, + "inputStyle" : "readWrite" + } +``` + +## Open Question: + +### 1. When Value is not provided by bot with "readWrite" style +There can be cases where bot chooses for "readWrite" style but it does not provide initial value for the Input Fields as it is not mandatory. How do we handle such cases? + +Possible options: + +1. we show `label` and `value` in horizotally aligned but value will be in `edit` mode only as the user first sees it. After user updated the field and move the focus away, then we can chow the value as just readable field. + +2. we do not support when `value` is not provided and we fallback to `default` style. + +### 2. How do we support inLineAction for Input.Text with "readWrite" style + +inlineAction is Input.Text specific property which allows a action button placed next to +value field which user can click to perform action related to that input field. inLineAction supports "Action.Submit", "Action.Execute", "Action.OpenUrl", "Action.ToggleVisibility" + + +``` +{ + "type": "Input.Text", + "id": "iconInlineActionId", + "label": "Text input with an inline action", + "inlineAction": { + "type": "Action.Submit", + "iconUrl": "https://adaptivecards.io/content/send.png", + "tooltip": "Send", + "isEnabled": true + } +} +``` +![img](../assets/InlineEditability/InlineEditabilitySecnario8.png) + + +How do we align this behavior with "readWrite" style? + +Possible options: +1. We show the inline action next to value field (all lable, value and inlineAction in horizontally aligned) but as disabled when user is not focussing/clicking on the Input field and when user performs any action, we show the inlineAction as enabled. + +2. We show the inline action next to value field (all lable, value and inlineAction in horizontally aligned) and the action will be enabled always just as current behvaior for inLine Action. + +3. We hide the inline action when user is not performing any action and when user clicks, we show the action. \ No newline at end of file