-
Notifications
You must be signed in to change notification settings - Fork 80
feat(input-time-picker): support fractional seconds #7532
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
Changes from 54 commits
275ad96
1f4ea0f
d2b6c27
187f7a2
b287eae
1956cd6
c5adde6
caf04e9
196b452
2d072e2
a68c024
cdb3e4e
d11cc4b
3ed449f
94befb6
4074f6a
9edd1f6
853794b
a1182e6
2c5f5a7
0bf9ee1
0081e1a
ba344c0
ee649ab
e0aac93
104e29a
3cfd06f
e1dcb13
0d7d3df
72d0da8
2b603c0
9ce3731
e8d85b7
2d007fb
19c9f09
6e7fb46
f9a88e6
21d4e65
333b7a9
27f3df8
7a6760c
bcf5875
d54325d
92af799
33593b6
80bbb68
e6a425d
5b5e522
138c01b
cb6c5c3
fc6e4f1
5a67658
9e31d89
f0a08d0
c245cd1
c02a4e2
fb371e2
57fb721
f643d33
02a29f6
22a8ce6
7b9108f
b5b8d64
31a4026
3bc9c30
e6c977c
3a7e8ff
22a3117
7899bb6
a738c32
32d7a72
b0f0a28
6c57565
171a709
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,6 +50,7 @@ import { | |
} from "../../utils/focusTrapComponent"; | ||
import { FocusTrap } from "focus-trap"; | ||
import { | ||
formatTimePart, | ||
formatTimeString, | ||
isValidTime, | ||
localizeTimeString, | ||
|
@@ -68,6 +69,7 @@ import localizedFormat from "dayjs/esm/plugin/localizedFormat"; | |
import preParsePostFormat from "dayjs/esm/plugin/preParsePostFormat"; | ||
import updateLocale from "dayjs/esm/plugin/updateLocale"; | ||
import { getSupportedLocale } from "../../utils/locale"; | ||
import { decimalPlaces } from "../../utils/math"; | ||
|
||
// some bundlers (e.g., Webpack) need dynamic import paths to be static | ||
const supportedDayJsLocaleToLocaleConfigImport = new Map([ | ||
|
@@ -129,6 +131,13 @@ dayjs.extend(localizedFormat); | |
dayjs.extend(preParsePostFormat); | ||
dayjs.extend(updateLocale); | ||
|
||
interface DayJSTimeParts { | ||
hour: number; | ||
minute: number; | ||
second: number; | ||
millisecond: number; | ||
} | ||
|
||
@Component({ | ||
tag: "calcite-input-time-picker", | ||
styleUrl: "input-time-picker.scss", | ||
|
@@ -323,6 +332,8 @@ export class InputTimePicker | |
|
||
private dialogId = `time-picker-dialog--${guid()}`; | ||
|
||
private localeConfig; | ||
eriklharper marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** whether the value of the input was changed as a result of user typing or not */ | ||
private userChangedValue = false; | ||
|
||
|
@@ -476,20 +487,107 @@ export class InputTimePicker | |
// we need to set the corresponding locale before parsing, otherwise it defaults to English (possible dayjs bug) | ||
dayjs.locale(this.effectiveLocale.toLowerCase()); | ||
|
||
const dayjsParseResult = dayjs(value, ["LTS", "LT"]); | ||
const nonFractionalSecondParts = this.delocalizeTimeStringToParts(value); | ||
|
||
if (dayjsParseResult.isValid()) { | ||
let unformattedTimeString = `${dayjsParseResult.get("hour")}:${dayjsParseResult.get( | ||
"minute" | ||
)}`; | ||
let delocalizedTimeString; | ||
|
||
if (this.shouldIncludeFractionalSeconds()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For readability, do you think you could flip this |
||
const stepPrecision = decimalPlaces(this.step); | ||
const centisecondParts = this.delocalizeTimeStringToParts(value, "S"); | ||
|
||
if (this.shouldIncludeSeconds()) { | ||
unformattedTimeString += `:${dayjsParseResult.get("seconds") || 0}`; | ||
if (stepPrecision === 1) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you look at simplifying this a bit? There does seem to be a bit of repetition between statement blocks. This can be tackled separately in a refactor PR. |
||
if (centisecondParts.millisecond !== 0) { | ||
delocalizedTimeString = this.getTimeStringFromParts(centisecondParts); | ||
} else { | ||
delocalizedTimeString = this.getTimeStringFromParts(nonFractionalSecondParts); | ||
} | ||
} else { | ||
const decisecondParts = this.delocalizeTimeStringToParts(value, "SS"); | ||
|
||
if (stepPrecision === 2) { | ||
if (decisecondParts.millisecond !== 0) { | ||
delocalizedTimeString = this.getTimeStringFromParts(decisecondParts); | ||
} else if (centisecondParts.millisecond !== 0) { | ||
delocalizedTimeString = this.getTimeStringFromParts(centisecondParts); | ||
} else { | ||
delocalizedTimeString = this.getTimeStringFromParts(nonFractionalSecondParts); | ||
} | ||
} else if (stepPrecision >= 3) { | ||
const millisecondParts = this.delocalizeTimeStringToParts(value, "SSS"); | ||
|
||
if (millisecondParts.millisecond !== 0) { | ||
delocalizedTimeString = this.getTimeStringFromParts(millisecondParts); | ||
} else if (decisecondParts.millisecond !== 0) { | ||
delocalizedTimeString = this.getTimeStringFromParts(decisecondParts); | ||
} else if (centisecondParts.millisecond !== 0) { | ||
delocalizedTimeString = this.getTimeStringFromParts(centisecondParts); | ||
} else { | ||
delocalizedTimeString = this.getTimeStringFromParts(nonFractionalSecondParts); | ||
} | ||
} | ||
} | ||
} else { | ||
delocalizedTimeString = this.getTimeStringFromParts(nonFractionalSecondParts); | ||
} | ||
|
||
return delocalizedTimeString; | ||
} | ||
|
||
private delocalizeTimeStringToParts( | ||
localizedTimeString: string, | ||
fractionalSecondFormatToken?: "S" | "SS" | "SSS" | ||
): DayJSTimeParts { | ||
const ltsFormatString = this.localeConfig?.formats?.LTS; | ||
const fractionalSecondTokenMatch = ltsFormatString.match(/ss\.*(S+)/g); | ||
|
||
if (fractionalSecondFormatToken && this.shouldIncludeFractionalSeconds()) { | ||
const secondFormatToken = `ss.${fractionalSecondFormatToken}`; | ||
this.localeConfig.formats.LTS = fractionalSecondTokenMatch | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion to DRY things a bit: this.localeConfig.formats.LTS = ltsFormatString.replace(
fractionalSecondTokenMatch ? fractionalSecondTokenMatch[0] : "ss",
secondFormatToken
); Applies to similar structures where the left side of the assignment is duplicated. |
||
? ltsFormatString.replace(fractionalSecondTokenMatch[0], secondFormatToken) | ||
: ltsFormatString.replace("ss", secondFormatToken); | ||
} else if (fractionalSecondTokenMatch) { | ||
this.localeConfig.formats.LTS = ltsFormatString.replace(fractionalSecondTokenMatch[0], "ss"); | ||
} | ||
|
||
dayjs.updateLocale( | ||
this.getSupportedDayJSLocale(getSupportedLocale(this.effectiveLocale)), | ||
this.localeConfig | ||
); | ||
|
||
const dayjsParseResult = dayjs(localizedTimeString, ["LTS", "LT"]); | ||
|
||
if (dayjsParseResult.isValid()) { | ||
return { | ||
hour: dayjsParseResult.get("hour"), | ||
minute: dayjsParseResult.get("minute"), | ||
second: dayjsParseResult.get("second"), | ||
millisecond: dayjsParseResult.get("millisecond"), | ||
}; | ||
} | ||
return { | ||
hour: null, | ||
minute: null, | ||
second: null, | ||
millisecond: null, | ||
}; | ||
} | ||
|
||
return formatTimeString(unformattedTimeString) || ""; | ||
private getTimeStringFromParts(parts: DayJSTimeParts): string { | ||
let timeString = ""; | ||
if (!parts) { | ||
return timeString; | ||
} | ||
return ""; | ||
if (parts.hour !== null && parts.minute !== null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For readability, there might be a way to reduce Applies to similar structures. |
||
timeString = `${formatTimePart(parts.hour)}:${formatTimePart(parts.minute)}`; | ||
if (this.shouldIncludeSeconds() && parts.second !== null) { | ||
timeString += `:${formatTimePart(parts.second)}`; | ||
if (this.shouldIncludeFractionalSeconds() && parts.millisecond !== null) { | ||
const second = (parts.millisecond * 0.001).toFixed(decimalPlaces(this.step)); | ||
timeString += `.${second.toString().replace("0.", "")}`; | ||
} | ||
} | ||
} | ||
return timeString; | ||
} | ||
|
||
private popoverCloseHandler = () => { | ||
|
@@ -531,17 +629,19 @@ export class InputTimePicker | |
|
||
const newValue = this.delocalizeTimeString(this.calciteInputEl.value); | ||
|
||
this.setValue(newValue); | ||
if (isValidTime(newValue)) { | ||
this.setValue(newValue); | ||
|
||
const localizedTimeString = localizeTimeString({ | ||
value: this.value, | ||
locale: this.effectiveLocale, | ||
numberingSystem: this.numberingSystem, | ||
includeSeconds: this.shouldIncludeSeconds(), | ||
}); | ||
const localizedTimeString = localizeTimeString({ | ||
value: this.value, | ||
locale: this.effectiveLocale, | ||
numberingSystem: this.numberingSystem, | ||
includeSeconds: this.shouldIncludeSeconds(), | ||
}); | ||
|
||
if (newValue && this.calciteInputEl.value !== localizedTimeString) { | ||
this.setInputValue(localizedTimeString); | ||
if (newValue && this.calciteInputEl.value !== localizedTimeString) { | ||
this.setInputValue(localizedTimeString); | ||
} | ||
} | ||
} else if (key === "ArrowDown") { | ||
this.open = true; | ||
|
@@ -554,22 +654,29 @@ export class InputTimePicker | |
} | ||
}; | ||
|
||
private getSupportedDayJSLocale(locale: string) { | ||
eriklharper marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be moved to |
||
const dayjsLocale = locale.toLowerCase(); | ||
if (dayjsLocale === "no") { | ||
return "nb"; | ||
} | ||
if (dayjsLocale === "pt-pt") { | ||
return "pt"; | ||
} | ||
return dayjsLocale; | ||
} | ||
|
||
private async loadDateTimeLocaleData(): Promise<void> { | ||
let supportedLocale = getSupportedLocale(this.effectiveLocale).toLowerCase(); | ||
|
||
if (supportedLocale === "no") { | ||
supportedLocale = "nb"; | ||
} | ||
|
||
if (supportedLocale === "pt-pt") { | ||
supportedLocale = "pt"; | ||
} | ||
supportedLocale = this.getSupportedDayJSLocale(supportedLocale); | ||
|
||
const { default: localeConfig } = await supportedDayJsLocaleToLocaleConfigImport.get( | ||
supportedLocale | ||
)(); | ||
|
||
dayjs.locale(localeConfig, null, true); | ||
this.localeConfig = localeConfig; | ||
|
||
dayjs.locale(this.localeConfig, null, true); | ||
dayjs.updateLocale(supportedLocale, this.getExtendedLocaleConfig(supportedLocale)); | ||
} | ||
|
||
|
@@ -656,6 +763,10 @@ export class InputTimePicker | |
return this.step < 60; | ||
} | ||
|
||
private shouldIncludeFractionalSeconds(): boolean { | ||
return decimalPlaces(this.step) > 0; | ||
} | ||
|
||
private setCalcitePopoverEl = (el: HTMLCalcitePopoverElement): void => { | ||
this.popoverEl = el; | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,8 @@ export const CSS = { | |
button: "button", | ||
buttonBottomLeft: "button--bottom-left", | ||
buttonBottomRight: "button--bottom-right", | ||
buttonFractionalSecondDown: "button--fractionalSecond-down", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you update There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not following. The pattern here is the same as the pattern elsewhere in the file for other items. |
||
buttonFractionalSecondUp: "button--fractionalSecond-up", | ||
buttonHourDown: "button--hour-down", | ||
buttonHourUp: "button--hour-up", | ||
buttonMeridiemDown: "button--meridiem-down", | ||
|
@@ -14,6 +16,7 @@ export const CSS = { | |
buttonTopRight: "button--top-right", | ||
column: "column", | ||
delimiter: "delimiter", | ||
fractionalSecond: "fractionalSecond", | ||
hour: "hour", | ||
input: "input", | ||
meridiem: "meridiem", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ import { newE2EPage } from "@stencil/core/testing"; | |
import { accessible, defaults, focusable, hidden, renders, t9n } from "../../tests/commonTests"; | ||
import { formatTimePart } from "../../utils/time"; | ||
import { CSS } from "./resources"; | ||
import { getElementXY } from "../../tests/utils"; | ||
|
||
const letterKeys = [ | ||
"a", | ||
|
@@ -1091,4 +1092,48 @@ describe("calcite-time-picker", () => { | |
|
||
expect(await page.find(`calcite-time-picker >>> .${CSS.second}`)).toBeNull(); | ||
}); | ||
|
||
describe("fractional second support", () => { | ||
it("upward nudge of empty fractional second sets to 0 for step=0.1", async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you also add coverage for the component's |
||
const page = await newE2EPage(); | ||
await page.setContent(`<calcite-time-picker step="0.1"></calcite-time-picker>`); | ||
const [buttonUpLocationX, buttonUpLocationY] = await getElementXY( | ||
page, | ||
"calcite-time-picker", | ||
".button--fractionalSecond-up" | ||
); | ||
await page.mouse.click(buttonUpLocationX, buttonUpLocationY); | ||
await page.waitForChanges(); | ||
const fractionalSecondEl = await page.find(`calcite-time-picker >>> .input.fractionalSecond`); | ||
expect(fractionalSecondEl.innerHTML).toEqual("0"); | ||
}); | ||
|
||
it("upward nudge of empty fractional second sets to 00 for step=0.01", async () => { | ||
const page = await newE2EPage(); | ||
await page.setContent(`<calcite-time-picker step="0.01"></calcite-time-picker>`); | ||
const [buttonUpLocationX, buttonUpLocationY] = await getElementXY( | ||
page, | ||
"calcite-time-picker", | ||
".button--fractionalSecond-up" | ||
); | ||
await page.mouse.click(buttonUpLocationX, buttonUpLocationY); | ||
await page.waitForChanges(); | ||
const fractionalSecondEl = await page.find(`calcite-time-picker >>> .input.fractionalSecond`); | ||
expect(fractionalSecondEl.innerHTML).toEqual("00"); | ||
}); | ||
|
||
it("upward nudge of empty fractional second sets to 000 for step=0.001", async () => { | ||
const page = await newE2EPage(); | ||
await page.setContent(`<calcite-time-picker step="0.001"></calcite-time-picker>`); | ||
const [buttonUpLocationX, buttonUpLocationY] = await getElementXY( | ||
page, | ||
"calcite-time-picker", | ||
".button--fractionalSecond-up" | ||
); | ||
await page.mouse.click(buttonUpLocationX, buttonUpLocationY); | ||
await page.waitForChanges(); | ||
const fractionalSecondEl = await page.find(`calcite-time-picker >>> .input.fractionalSecond`); | ||
expect(fractionalSecondEl.innerHTML).toEqual("000"); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be done as a follow-up, but can you move this to a new
interfaces.d.ts
file?