Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(time-picker): add build-in validation for partial input value #1166

Merged
merged 16 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
6cedd3c
feat(time-picker): add build-in validation for partial input value
bualoy-napat May 20, 2024
101f522
docs(time-picker): add example for error-changed
bualoy-napat May 21, 2024
3fa256f
Merge branch 'v6' into feat/error-state-for-partial-input-v6
wattachai-lseg May 23, 2024
469b058
docs(time-picker): update live example input validation
bualoy-napat May 29, 2024
e7b8ec3
refactor(time-picker): simplify partial validation method
bualoy-napat May 29, 2024
10609e2
test(time-picker): update unit test to mock user interaction
bualoy-napat May 29, 2024
0ce4ed7
refactor(time-picker): refactor time-picker validity checks and relat…
bualoy-napat May 31, 2024
3e2e5aa
refactor(datetime-picker): refactored validity checks in time-picker …
bualoy-napat May 31, 2024
32321ba
fix(time-picker): incorrect border color in error, readonly, disabled…
bualoy-napat Jun 3, 2024
b2e6e72
refactor(time-picker): remove unused tag in demo
bualoy-napat Jun 3, 2024
b123473
docs(time-picker): sync content from v7
bualoy-napat Jun 3, 2024
590eea3
fix(time-picker): incorrect hover color in halo theme
bualoy-napat Jun 4, 2024
01335c0
refactor(time-picker): call isShowSeconds instead of showSeconds
bualoy-napat Jun 4, 2024
ea81b1e
fix(time-picker): toggle method should not fire value-changed event
bualoy-napat Jun 6, 2024
2211d9a
test(time-picker): add test case to cover tap toggle behavior
bualoy-napat Jun 6, 2024
c4eafb8
Merge branch 'v6' into feat/error-state-for-partial-input-v6
wattachai-lseg Jun 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions documents/src/pages/elements/time-picker.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,81 @@ utcTimePicker.hours = date.getUTCHours();
utcTimePicker.minutes = date.getUTCMinutes();
```

## Input validation
`ef-time-picker` has validation logic similar to a [native input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/time). When a user types a partial value into the control, error style will be shown to notify the user.

You can call `reportValidity()` to trigger the validation anytime, and it will set error style if input is partial. In case that the input is initially or programmatically set to an invalid value, you must call `reportValidity()` to show the error style. Make sure that the element has been defined before calling the method.

Whenever input is invalid, the `error` attribute will be added to the element. You can use the `error` property to check whether input is currently in the error state or not.
If the error state is changed (not programmatically), an `error-changed` event will be dispatched along with the current error state.

::
```javascript
::import-elements::
const errorStatus = document.querySelector('p');
const el = document.querySelector('ef-time-picker');

el.addEventListener('error-changed', (event) => {
errorStatus.textContent = event.detail.value ? 'error due to partial input' : '';
});
```
```css
p {
color: red;
}
```
```html
<div>
<ef-time-picker></ef-time-picker>
<p></p>
</div>
```
::

### Custom validation
For advance use cases, default validation and error state of the field can be overridden.
To do this, make sure that `custom-validation` is set,
then validate with your customised validation logic and update `error` property accordingly.

::
```javascript
::import-elements::
const errorNotice = document.getElementById('error-notice');
const el = document.querySelector('ef-time-picker');

el.addEventListener('value-changed', (event) => {
const targetEl = event.target;
if ((targetEl.hours < 8) || (targetEl.hours >= 17 && targetEl.minutes > 0)) {
errorNotice.textContent = 'Not in the working hour';
targetEl.error = true;
} else {
errorNotice.textContent = '';
targetEl.error = false;
}
});

el.addEventListener('blur', (event) => {
const targetEl = event.target;
if (!targetEl.hours || !targetEl.minutes) {
errorNotice.textContent = 'Please choose time';
targetEl.error = true;
}
});
```
```css
#error-notice {
color: red;
}
```
```html
<div>
<p>Please choose a time to receive service (Service hours 8:00-17:00)</p>
<ef-time-picker></ef-time-picker>
<p id="error-notice"></p>
</div>
```
::

## Combining time and date

Typically, the time value must be combined with a date object in order to use an API. To do this, use methods on the native `Date` object.
Expand Down
11 changes: 11 additions & 0 deletions packages/elemental-theme/src/custom-elements/ef-time-picker.less
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@
&:extend(:host[focused]); // Extend ef-number-field[focused]
}

:host[error] {
&:extend(:host[error]); // Extend ef-number-field[error]
[part='input'],
[part='toggle'] {
&:focus::after,
&[focused]::after {
border-bottom-color: @scheme-color-error;
}
}
}

:host[disabled] {
&:extend(:host[disabled]); // Extend ef-number-field[disabled]
}
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@
</div>
<div part="timepicker-wrapper">
<ef-time-picker
custom-validation=""
id="timepicker"
part="time-picker"
role="group"
Expand Down Expand Up @@ -349,6 +350,7 @@
</div>
<div part="timepicker-wrapper">
<ef-time-picker
custom-validation=""
id="timepicker"
part="time-picker"
role="group"
Expand Down Expand Up @@ -432,6 +434,7 @@
</div>
<div part="timepicker-wrapper">
<ef-time-picker
custom-validation=""
id="timepicker-from"
part="time-picker"
role="group"
Expand All @@ -441,6 +444,7 @@
<div part="input-separator">
</div>
<ef-time-picker
custom-validation=""
id="timepicker-to"
part="time-picker"
role="group"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,36 @@ describe('datetime-picker/Value', function () {
'error state should return to false when both inputs value are valid'
);
});
it('It should fall back timepicker value to valid when popup is opened', async function () {
const el = await fixture(
'<ef-datetime-picker timepicker value="2024-05-10T11:00" lang="en-gb" opened></ef-datetime-picker>'
);

el.timepickerEl.hours = null;
el.opened = false;
await elementUpdated(el);

el.opened = true;
await elementUpdated(el);

expect(el.timepickerEl.value).to.equal('11:00');
});
it('It should fall back timepicker values to valid when popup is opened in range mode', async function () {
const el = await fixture(
'<ef-datetime-picker range timepicker values="2024-05-10T11:00,2024-05-11T15:00" lang="en-gb" opened></ef-datetime-picker>'
);

el.timepickerFromEl.hours = null;
el.timepickerToEl.minutes = null;
el.opened = false;
await elementUpdated(el);

el.opened = true;
await elementUpdated(el);

expect(el.timepickerFromEl.value).to.equal('11:00');
expect(el.timepickerToEl.value).to.equal('15:00');
});
// TODO: add input validation test cases when the value update is originated from typing input
});
});
30 changes: 30 additions & 0 deletions packages/elements/src/datetime-picker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ export class DatetimePicker extends FormFieldElement implements MultiValue {
protected override update(changedProperties: PropertyValues): void {
if (changedProperties.has('opened') && this.opened) {
this.lazyRendered = true;
this.syncTimePickerInput();
}
// make sure to close popup for disabled
if (this.opened && !this.canOpenPopup) {
Expand Down Expand Up @@ -538,6 +539,34 @@ export class DatetimePicker extends FormFieldElement implements MultiValue {
void super.performUpdate();
}

/**
* if the time-picker input(s) is invalid
* it will sync time-picker value to previous valid value that store in datetime-picker
* @returns {void}
*/
private syncTimePickerInput(): void {
if (!this.timepicker || !this.opened) {
return;
}

const validateAndFallback = (element: TimePicker | null | undefined, value: string) => {
if (!element) {
return;
}

if (!element.checkValidity() || (!element.value && value)) {
element.value = value;
}
};

if (this.range) {
validateAndFallback(this.timepickerFromEl, this.timepickerValues[0]);
validateAndFallback(this.timepickerToEl, this.timepickerValues[1]);
} else {
validateAndFallback(this.timepickerEl, this.timepickerValues[0]);
}
}

/**
* Overwrite validation method for value
*
Expand Down Expand Up @@ -1220,6 +1249,7 @@ export class DatetimePicker extends FormFieldElement implements MultiValue {
return html`<ef-time-picker
id="${id}"
part="time-picker"
custom-validation
.showSeconds=${this.showSeconds}
.amPm=${this.amPm}
.value=${value}
Expand Down
50 changes: 44 additions & 6 deletions packages/elements/src/time-picker/__demo__/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
<demo-block header="Value" layout="normal" tags="value">
<ef-time-picker value="13:30:25" am-pm></ef-time-picker>
</demo-block>
<demo-block header="Specifig segment value" layout="normal" tags="hours, miutes, seconds">
<demo-block header="Specific segment value" layout="normal" tags="hours, miutes, seconds">
<ef-time-picker hours="20" minutes="50" seconds="30" show-seconds></ef-time-picker>
</demo-block>
<demo-block header="Readonly" layout="normal" tags="readonly">
Expand All @@ -56,22 +56,60 @@
<demo-block header="Disabled" layout="normal" tags="disabled">
<ef-time-picker value="13:30:25" disabled></ef-time-picker>
</demo-block>
<demo-block header="Custom validation" layout="normal" tags="custom-validation">
<p>Please choose a time to receive service (Service hours 8:00-17:00)</p>
<ef-time-picker id="custom-validation" custom-validation></ef-time-picker>
<p id="error-notice"></p>
<script>
const errorNotice = document.getElementById('error-notice');
const el = document.getElementById('custom-validation');

el.addEventListener('value-changed', (event) => {
const targetEl = event.target;
if (targetEl.hours < 8 || (targetEl.hours >= 17 && targetEl.minutes > 0)) {
errorNotice.textContent = 'Not in the working hour';
targetEl.error = true;
} else {
errorNotice.textContent = '';
targetEl.error = false;
}
});

el.addEventListener('blur', (event) => {
const targetEl = event.target;
if (!targetEl.hours || !targetEl.minutes) {
errorNotice.textContent = 'Please choose time';
targetEl.error = true;
}
});
</script>
</demo-block>
<demo-block id="event" header="Event" layout="normal" tags="event">
<p>Value-changed event and error-change event</p>
<ef-time-picker value="15:30:25"></ef-time-picker>
<ef-time-picker value="15:30:25" am-pm></ef-time-picker>
<div>
<span>value-changed: </span>
<input id="log" readonly />
</div>
<div>
<span>error-changed: </span>
<input id="error-log" readonly />
</div>
<script>
(function () {
const log = document.getElementById('log');
const valueLog = document.getElementById('log');
const errorLog = document.getElementById('error-log');
const onValueChanged = (event) => {
log.value = JSON.stringify(event.detail);
valueLog.value = JSON.stringify(event.detail);
};
const onErrorChanged = (event) => {
errorLog.value = JSON.stringify(event.detail);
};
document
.querySelectorAll('#event ef-time-picker')
.forEach((element) => element.addEventListener('value-changed', onValueChanged));
document.querySelectorAll('#event ef-time-picker').forEach((element) => {
element.addEventListener('value-changed', onValueChanged);
element.addEventListener('error-changed', onErrorChanged);
});
})();
</script>
</demo-block>
Expand Down
88 changes: 88 additions & 0 deletions packages/elements/src/time-picker/__test__/time-picker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,94 @@ describe('time-picker/TimePicker', function () {
await elementUpdated(el);
expect(el.seconds).to.equal(null);
});
it('should set error state to false when reportValidity is called without value', async function () {
const el = await fixture('<ef-time-picker error show-seconds></ef-time-picker>');
const validity = el.reportValidity();
expect(el.error).to.be.equal(false);
expect(validity).to.be.equal(true);
});
it('should set error state to true when reportValidity is called with partial value', async function () {
const el = await fixture('<ef-time-picker hours="12" show-seconds></ef-time-picker>');
const validity = el.reportValidity();
expect(el.error).to.be.equal(true);
expect(validity).to.be.equal(false);
});
it('should set error state to false when reportValidity is called with valid values', async function () {
const el = await fixture('<ef-time-picker value="12:11:10" error show-seconds></ef-time-picker>');
const validity = el.reportValidity();
expect(el.error).to.be.equal(false);
expect(validity).to.be.equal(true);
});
it('should add error state when value is partial by a mock user interaction', async function () {
const el = await fixture(timePickerDefaults);
el.hoursInput.value = '12';
setTimeout(() => el.hoursInput.dispatchEvent(new Event('input')));
await oneEvent(el.hoursInput, 'input');
expect(el.error).to.be.equal(true);
});
it('should remove error state when value is not partial by a mock user interaction', async function () {
const el = await fixture(timePickerDefaults);
el.hoursInput.value = '12';
setTimeout(() => el.hoursInput.dispatchEvent(new Event('input')));
await oneEvent(el.hoursInput, 'input');
expect(el.error).to.be.equal(true);

el.minutesInput.value = '00';
setTimeout(() => el.minutesInput.dispatchEvent(new Event('input')));
await oneEvent(el.minutesInput, 'input');
expect(el.error).to.be.equal(false);
});
it('should add error state when value is partial with show seconds by a mock user interaction', async function () {
const el = await fixture('<ef-time-picker show-seconds></ef-time-picker>');
el.hoursInput.value = '12';
el.minutesInput.value = '00';
setTimeout(() => {
el.hoursInput.dispatchEvent(new Event('input'));
el.minutesInput.dispatchEvent(new Event('input'));
});
await Promise.all([oneEvent(el.minutesInput, 'input'), oneEvent(el.hoursInput, 'input')]);
expect(el.error).to.be.equal(true);
});
it('should remove error state when value is not partial with show seconds by a mock user interaction', async function () {
const el = await fixture('<ef-time-picker show-seconds></ef-time-picker>');
el.hoursInput.value = '12';
el.minutesInput.value = '00';
setTimeout(() => {
el.hoursInput.dispatchEvent(new Event('input'));
el.minutesInput.dispatchEvent(new Event('input'));
});
await Promise.all([oneEvent(el.minutesInput, 'input'), oneEvent(el.hoursInput, 'input')]);
expect(el.error).to.be.equal(true);

el.secondsInput.value = '00';
setTimeout(() => el.secondsInput.dispatchEvent(new Event('input')));
await oneEvent(el.secondsInput, 'input');
expect(el.error).to.be.equal(false);
});
it('should not add error state when remove all segments by a mock user interaction', async function () {
const el = await fixture('<ef-time-picker value="12:00:00" show-seconds></ef-time-picker>');
el.hoursInput.value = '';
setTimeout(() => el.hoursInput.dispatchEvent(new Event('input')));
await oneEvent(el.hoursInput, 'input');
expect(el.error).to.be.equal(true);

el.minutesInput.value = '';
setTimeout(() => el.minutesInput.dispatchEvent(new Event('input')));
await oneEvent(el.minutesInput, 'input');
expect(el.error).to.be.equal(true);

el.secondsInput.value = '';
setTimeout(() => el.secondsInput.dispatchEvent(new Event('input')));
await oneEvent(el.secondsInput, 'input');
expect(el.error).to.be.equal(false);
});
it('should add error state when type invalid value by a mock user interaction', async function () {
const el = await fixture('<ef-time-picker value="12:10:08" show-seconds></ef-time-picker>');
el.secondsInput.value = '88';
setTimeout(() => el.secondsInput.dispatchEvent(new Event('input')));
await oneEvent(el.secondsInput, 'input');
expect(el.error).to.be.equal(true);
});
});

describe('Modes', function () {
Expand Down
Loading