Skip to content

Commit df57f06

Browse files
authored
fix(ui5-time-picker): display value state message in popover headers (#10582)
Previously, the `valueStateMessage` was displayed only below or above the input field, making it less clear when a `valueState` was active. With this enhancement, the `valueStateMessage` and its associated `valueState` styling now also appear in the header of the time selection popover, providing better visibility of the `valueState` and its provided message.
1 parent c9d7f3f commit df57f06

10 files changed

+199
-36
lines changed

Diff for: packages/main/src/DateTimeInput.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
2+
3+
// Styles
4+
import Input from "./Input.js";
5+
import { property } from "@ui5/webcomponents-base/dist/decorators.js";
6+
7+
/**
8+
* Extention of the UI5 Input, so we do not modify Input's private properties within the datetime components.
9+
* Intended to be used for the DateTime components.
10+
*
11+
* @class
12+
* @private
13+
*/
14+
@customElement({
15+
tag: "ui5-datetime-input",
16+
})
17+
18+
class DateTimeInput extends Input {
19+
@property({ noAttribute: true })
20+
_shouldOpenValueStatePopover = false;
21+
22+
/**
23+
* Prevents the value state message popover from appearing when a responsive popover (like time selection) is open
24+
* since the responsive popover already includes the necessary information in its header.
25+
*
26+
* @protected
27+
* @override
28+
*/
29+
get hasValueStateMessage() {
30+
return this._shouldOpenValueStatePopover && super.hasValueStateMessage;
31+
}
32+
}
33+
34+
DateTimeInput.define();
35+
36+
export default DateTimeInput;

Diff for: packages/main/src/TimePicker.ts

+55-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
66
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
77
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
88
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
9+
import willShowContent from "@ui5/webcomponents-base/dist/util/willShowContent.js";
910
import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js";
1011
import { submitForm } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js";
1112
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
@@ -29,7 +30,6 @@ import {
2930
isF6Next,
3031
isF6Previous,
3132
} from "@ui5/webcomponents-base/dist/Keys.js";
32-
import "@ui5/webcomponents-icons/dist/time-entry-request.js";
3333
import UI5Date from "@ui5/webcomponents-localization/dist/dates/UI5Date.js";
3434
import type Popover from "./Popover.js";
3535
import type ResponsivePopover from "./ResponsivePopover.js";
@@ -45,12 +45,19 @@ import {
4545
TIMEPICKER_INPUT_DESCRIPTION,
4646
TIMEPICKER_POPOVER_ACCESSIBLE_NAME,
4747
FORM_TEXTFIELD_REQUIRED,
48+
VALUE_STATE_ERROR,
49+
VALUE_STATE_INFORMATION,
50+
VALUE_STATE_SUCCESS,
51+
VALUE_STATE_WARNING,
4852
} from "./generated/i18n/i18n-defaults.js";
4953

5054
// Styles
5155
import TimePickerCss from "./generated/themes/TimePicker.css.js";
5256
import TimePickerPopoverCss from "./generated/themes/TimePickerPopover.css.js";
5357
import ResponsivePopoverCommonCss from "./generated/themes/ResponsivePopoverCommon.css.js";
58+
import ValueStateMessageCss from "./generated/themes/ValueStateMessage.css.js";
59+
60+
type ValueStateAnnouncement = Record<Exclude<ValueState, ValueState.None>, string>;
5461

5562
type TimePickerChangeInputEventDetail = {
5663
value: string,
@@ -133,6 +140,7 @@ type TimePickerInputEventDetail = TimePickerChangeInputEventDetail;
133140
TimePickerCss,
134141
ResponsivePopoverCommonCss,
135142
TimePickerPopoverCss,
143+
ValueStateMessageCss,
136144
],
137145
})
138146
/**
@@ -697,6 +705,26 @@ class TimePicker extends UI5Element implements IFormInputElement {
697705
}
698706
}
699707

708+
get valueStateDefaultText(): string | undefined {
709+
if (this.valueState === ValueState.None) {
710+
return;
711+
}
712+
713+
return this.valueStateTextMappings[this.valueState];
714+
}
715+
716+
get valueStateTextMappings(): ValueStateAnnouncement {
717+
return {
718+
[ValueState.Positive]: TimePicker.i18nBundle.getText(VALUE_STATE_SUCCESS),
719+
[ValueState.Negative]: TimePicker.i18nBundle.getText(VALUE_STATE_ERROR),
720+
[ValueState.Critical]: TimePicker.i18nBundle.getText(VALUE_STATE_WARNING),
721+
[ValueState.Information]: TimePicker.i18nBundle.getText(VALUE_STATE_INFORMATION),
722+
};
723+
}
724+
725+
get shouldDisplayDefaultValueStateMessage(): boolean {
726+
return !willShowContent(this.valueStateMessage) && this.hasValueStateText;
727+
}
700728
get submitButtonLabel() {
701729
return TimePicker.i18nBundle.getText(TIMEPICKER_SUBMIT_BUTTON);
702730
}
@@ -705,6 +733,32 @@ class TimePicker extends UI5Element implements IFormInputElement {
705733
return TimePicker.i18nBundle.getText(TIMEPICKER_CANCEL_BUTTON);
706734
}
707735

736+
get hasValueStateText(): boolean {
737+
return this.hasValueState && this.valueState !== ValueState.Positive;
738+
}
739+
740+
get hasValueState(): boolean {
741+
return this.valueState !== ValueState.None;
742+
}
743+
744+
get classes() {
745+
return {
746+
popover: {
747+
"ui5-suggestions-popover": true,
748+
"ui5-popover-with-value-state-header-phone": this._isPhone && this.hasValueStateText,
749+
"ui5-popover-with-value-state-header": !this._isPhone && this.hasValueStateText,
750+
},
751+
popoverValueState: {
752+
"ui5-valuestatemessage-header": true,
753+
"ui5-valuestatemessage-root": true,
754+
"ui5-valuestatemessage--success": this.valueState === ValueState.Positive,
755+
"ui5-valuestatemessage--error": this.valueState === ValueState.Negative,
756+
"ui5-valuestatemessage--warning": this.valueState === ValueState.Critical,
757+
"ui5-valuestatemessage--information": this.valueState === ValueState.Information,
758+
},
759+
};
760+
}
761+
708762
/**
709763
* @protected
710764
*/

Diff for: packages/main/src/TimePickerPopoverTemplate.tsx

+48-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import type TimePicker from "./TimePicker.js";
22
import Button from "./Button.js";
33
import Popover from "./Popover.js";
4+
import Icon from "./Icon.js";
45
import ResponsivePopover from "./ResponsivePopover.js";
56
import TimeSelectionClocks from "./TimeSelectionClocks.js";
67
import TimeSelectionInputs from "./TimeSelectionInputs.js";
8+
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
9+
import error from "@ui5/webcomponents-icons/dist/error.js";
10+
import alert from "@ui5/webcomponents-icons/dist/alert.js";
11+
import sysEnter2 from "@ui5/webcomponents-icons/dist/sys-enter-2.js";
12+
import information from "@ui5/webcomponents-icons/dist/information.js";
713

814
export default function TimePickerPopoverTemplate(this: TimePicker) {
915
return (
@@ -16,14 +22,16 @@ export default function TimePickerPopoverTemplate(this: TimePicker) {
1622
opener={this}
1723
open={this.open}
1824
allowTargetOverlap={true}
19-
_hideHeader={true}
25+
_hideHeader={!this.hasValueStateText}
2026
hideArrow={true}
2127
accessibleName={this.pickerAccessibleName}
2228
onClose={this.onResponsivePopoverAfterClose}
2329
onOpen={this.onResponsivePopoverAfterOpen}
2430
onWheel={this._handleWheel}
2531
onKeyDown={this._onkeydown}
2632
>
33+
{this.hasValueStateText && valueStateTextHeader.call(this)}
34+
2735
<TimeSelectionClocks
2836
id={`${this._id}-time-sel`}
2937
value={this._timeSelectionValue}
@@ -52,6 +60,8 @@ export default function TimePickerPopoverTemplate(this: TimePicker) {
5260
onWheel={this._handleWheel}
5361
onKeyDown={this._onkeydown}
5462
>
63+
{this.hasValueStateText && valueStateTextHeader.call(this, { "width": "100%" }) }
64+
5565
<div class="popover-content">
5666
<TimeSelectionInputs
5767
id={`${this._id}-time-sel-inputs`}
@@ -71,3 +81,40 @@ export default function TimePickerPopoverTemplate(this: TimePicker) {
7181
</>
7282
);
7383
}
84+
85+
function valueStateMessage(this: TimePicker) {
86+
return (
87+
this.shouldDisplayDefaultValueStateMessage ? this.valueStateDefaultText : <slot name="valueStateMessage"></slot>
88+
);
89+
}
90+
91+
function valueStateTextHeader(this: TimePicker, style?: Record<string, string>) {
92+
if (!this.hasValueStateText) {
93+
return;
94+
}
95+
96+
return (
97+
<div
98+
slot="header"
99+
class={{
100+
"ui5-popover-header": true,
101+
...this.classes.popoverValueState,
102+
}}
103+
style={style}
104+
>
105+
<Icon class="ui5-input-value-state-message-icon" name={valueStateMessageInputIcon.call(this)}/>
106+
{ valueStateMessage.call(this) }
107+
</div>
108+
);
109+
}
110+
111+
function valueStateMessageInputIcon(this: TimePicker) {
112+
const iconPerValueState = {
113+
Negative: error,
114+
Critical: alert,
115+
Positive: sysEnter2,
116+
Information: information,
117+
};
118+
119+
return this.valueState !== ValueState.None ? iconPerValueState[this.valueState] : "";
120+
}

Diff for: packages/main/src/TimePickerTemplate.tsx

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import type TimePicker from "./TimePicker.js";
22
import Icon from "./Icon.js";
3-
import Input from "./Input.js";
3+
import DateTimeInput from "./DateTimeInput.js";
44
import TimePickerPopoverTemplate from "./TimePickerPopoverTemplate.js";
5+
import timeEntryRequest from "@ui5/webcomponents-icons/dist/time-entry-request.js";
56

67
export default function TimePickerTemplate(this: TimePicker) {
78
return (
89
<>
910
<div id={this._id} class="ui5-time-picker-root">
10-
<Input
11+
<DateTimeInput
1112
data-sap-focus-ref
1213
id={`${this._id}-inner`}
1314
class="ui5-time-picker-input"
@@ -17,14 +18,15 @@ export default function TimePickerTemplate(this: TimePicker) {
1718
readonly={this.readonly}
1819
required={this.required}
1920
valueState={this.valueState}
21+
_shouldOpenValueStatePopover={!this.open}
2022
_inputAccInfo={this.accInfo}
2123
onClick={this._handleInputClick}
2224
onChange={this._handleInputChange}
2325
onInput={this._handleInputLiveChange}
2426
onFocusIn={this._onfocusin}
2527
onKeyDown={this._onkeydown}
2628
>
27-
{this.valueStateMessage.length > 0 &&
29+
{this.valueStateMessage.length > 0 && !this.open &&
2830
<slot
2931
name="valueStateMessage"
3032
slot="valueStateMessage"
@@ -34,7 +36,7 @@ export default function TimePickerTemplate(this: TimePicker) {
3436
{!this.readonly &&
3537
<Icon
3638
slot="icon"
37-
name={this.openIconName}
39+
name={timeEntryRequest}
3840
tabindex={-1}
3941
showTooltip={true}
4042
onClick={this._togglePicker}
@@ -45,7 +47,7 @@ export default function TimePickerTemplate(this: TimePicker) {
4547
}}
4648
/>
4749
}
48-
</Input>
50+
</DateTimeInput>
4951
</div>
5052

5153
{ TimePickerPopoverTemplate.call(this) }

Diff for: packages/main/src/themes/TimePicker.css

+4
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,8 @@
4747
word-spacing: inherit;
4848
margin: inherit;
4949
height: inherit;
50+
}
51+
52+
.ui5-time-picker-popover::part(header) {
53+
padding: 0;
5054
}

Diff for: packages/main/src/themes/TimePickerPopover.css

+5
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@
1717
.ui5-time-picker-popover::part(content) {
1818
padding: 0;
1919
}
20+
21+
.ui5-time-picker-inputs-popover::part(header) {
22+
padding: 0;
23+
width: 100%;
24+
}

Diff for: packages/main/test/pages/TimePicker.html

+15
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,21 @@ <h3>TimePicker in Compact</h3>
7575
</div>
7676
</section>
7777

78+
<ui5-title>Test valueStateMessage in header</ui5-title>
79+
<ui5-time-picker id="timepickerValueStateMessage" value-state="Negative">
80+
<div slot="valueStateMessage" id="customValueStateMessage">Please provide valid value</div>
81+
</ui5-time-picker>
82+
83+
<ui5-time-picker></ui5-time-picker>
84+
85+
<ui5-time-picker id="timepickerValueStateMessageCritical" value-state="Critical">
86+
<div slot="valueStateMessage" id="customValueStateMessageCritical">Please check this value</div>
87+
</ui5-time-picker>
88+
89+
<ui5-time-picker id="timepickerValueStateMessageInformation" value-state="Information">
90+
<div slot="valueStateMessage" id="customValueStateMessageInformation">Additional information</div>
91+
</ui5-time-picker>
92+
7893
<script>
7994
var counter = 0;
8095
timepickerChange.addEventListener("ui5-change", function() {

Diff for: packages/main/test/specs/DateControlsWithTimezone.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe("Calendar general interaction", () => {
2323
it("The time is with the correct offset in time picker", async () => {
2424

2525
const timePicker = await browser.$("#timePickerNow");
26-
const timePickerValue = await timePicker.shadow$("ui5-input").getValue();
26+
const timePickerValue = await timePicker.shadow$("ui5-datetime-input").getValue();
2727
const now = new Date();
2828
const offset = now.getTimezoneOffset();
2929
now.setMinutes(now.getMinutes() + offset);

Diff for: packages/main/test/specs/TimePicker.mobile.spec.js

+8-8
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe("TimePicker on phone - general interactions", () => {
3636

3737
// act
3838
await timePicker.setProperty("value", "11:12:13");
39-
await timePicker.shadow$("ui5-input").click();
39+
await timePicker.shadow$("ui5-datetime-input").click();
4040

4141
// assert
4242
assert.ok(await timePickerPopover.getAttribute("open"), "Popover found");
@@ -61,7 +61,7 @@ describe("TimePicker on phone - general interactions", () => {
6161

6262
// act
6363
await timePicker.setProperty("value", "10:20:30 AM");
64-
await timePicker.shadow$("ui5-input").click();
64+
await timePicker.shadow$("ui5-datetime-input").click();
6565

6666
await hoursInnerInput.setValue("11");
6767
await minutesInnerInput.setValue("22");
@@ -80,7 +80,7 @@ describe("TimePicker on phone - general interactions", () => {
8080
assert.strictEqual((await timePicker.getAttribute("value")).toUpperCase(), "11:22:33 PM", "Correct new time is set to the TimePicker");
8181

8282
// act
83-
await timePicker.shadow$("ui5-input").click();
83+
await timePicker.shadow$("ui5-datetime-input").click();
8484
await hoursInnerInput.setValue("10");
8585
await minutesInnerInput.setValue("20");
8686
await secondsInnerInput.setValue("30");
@@ -109,7 +109,7 @@ describe("TimePicker on phone - general interactions", () => {
109109

110110
// act
111111
await timePicker.setProperty("value", "10:20:30 AM");
112-
await timePicker.shadow$("ui5-input").click();
112+
await timePicker.shadow$("ui5-datetime-input").click();
113113
await browser.keys(["0", "8", "2", "4", "1", "3"]);
114114

115115
// assert
@@ -124,7 +124,7 @@ describe("TimePicker on phone - general interactions", () => {
124124
assert.strictEqual((await timePicker.getAttribute("value")).toUpperCase(), "08:24:13", "New time is not set to the TimePicker");
125125

126126
// act
127-
await timePicker.shadow$("ui5-input").click();
127+
await timePicker.shadow$("ui5-datetime-input").click();
128128
await browser.keys(["3", "6", "8"]);
129129

130130
// assert
@@ -139,7 +139,7 @@ describe("TimePicker on phone - general interactions", () => {
139139
assert.strictEqual((await timePicker.getAttribute("value")).toUpperCase(), "03:06:08", "New time is not set to the TimePicker");
140140

141141
// act
142-
await timePicker.shadow$("ui5-input").click();
142+
await timePicker.shadow$("ui5-datetime-input").click();
143143
await browser.keys(["4", "5"]);
144144

145145
// assert
@@ -188,7 +188,7 @@ describe("TimePicker on phone - accessibility and other input attributes", () =>
188188
const texts = await getResourceBundleTexts({ keys, id: "timepicker" });
189189

190190
// act
191-
await timePicker.shadow$("ui5-input").click();
191+
await timePicker.shadow$("ui5-datetime-input").click();
192192

193193
// assert
194194
assert.strictEqual(await hoursInnerInput.getAttribute("step"), "1", "Correct hours 'step' attribute");
@@ -217,7 +217,7 @@ describe("TimePicker on phone - accessibility and other input attributes", () =>
217217
const secondsInnerInput = await components[2].shadow$("input");
218218

219219
// act
220-
await timePicker.shadow$("ui5-input").click();
220+
await timePicker.shadow$("ui5-datetime-input").click();
221221

222222
// assert
223223
assert.strictEqual(await hoursInnerInput.getAttribute("type"), "number", "Correct hours 'type' attribute");

0 commit comments

Comments
 (0)