@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
341 lines (340 loc) • 19.6 kB
JavaScript
/* COPYRIGHT Esri - https://js.arcgis.com/5.0/LICENSE.txt */
import { c as customElement } from "../../chunks/runtime.js";
import { css, html } from "lit";
import { LitElement, createEvent, safeClassMap } from "@arcgis/lumina";
import { createRef, ref } from "lit/directives/ref.js";
import { i as isValidNumber } from "../../chunks/locale.js";
import { c as componentFocusable, g as getIconScale } from "../../chunks/component.js";
import { d as decimalPlaces } from "../../chunks/math.js";
import { g as getElementDir } from "../../chunks/dom.js";
import { u as useT9n } from "../../chunks/useT9n.js";
import { u as useSetFocus } from "../../chunks/useSetFocus.js";
import { u as useTime } from "../../chunks/useTime.js";
const CSS = {
button: "button",
buttonBottomLeft: "button--bottom-left",
buttonBottomRight: "button--bottom-right",
buttonFractionalSecondDown: "button--fractionalSecond-down",
buttonFractionalSecondUp: "button--fractionalSecond-up",
buttonHourDown: "button--hour-down",
buttonHourUp: "button--hour-up",
buttonMeridiemDown: "button--meridiem-down",
buttonMeridiemUp: "button--meridiem-up",
buttonMinuteDown: "button--minute-down",
buttonMinuteUp: "button--minute-up",
buttonSecondDown: "button--second-down",
buttonSecondUp: "button--second-up",
buttonTopLeft: "button--top-left",
buttonTopRight: "button--top-right",
column: "column",
decimalSeparator: "decimal-separator",
delimiter: "delimiter",
fractionalSecond: "fractionalSecond",
hour: "hour",
hourSuffix: "hour-suffix",
input: "input",
inputFocus: "inputFocus",
meridiem: "meridiem",
minute: "minute",
minuteSuffix: "minute-suffix",
second: "second",
secondSuffix: "second-suffix",
showMeridiem: "show-meridiem",
showSecond: "show-second",
scale: (scale) => `scale-${scale}`,
timePicker: "time-picker",
meridiemStart: "meridiem--start"
};
const ICONS = {
chevronUp: "chevron-up",
chevronDown: "chevron-down"
};
const styles = css`:host{display:inline-block}.time-picker{display:flex;-webkit-user-select:none;user-select:none;align-items:center;border-width:1px;border-style:solid;font-weight:var(--calcite-font-weight-medium);border-color:var(--calcite-time-picker-border-color, var(--calcite-color-border-3));border-radius:var(--calcite-time-picker-corner-radius, var(--calcite-border-radius-round));color:var(--calcite-time-picker-color, var(--calcite-color-text-1));background-color:var(--calcite-time-picker-background-color, var(--calcite-color-foreground-1));overflow:hidden}.time-picker .column{display:flex;flex-direction:column}.time-picker .meridiem--start{order:-1}.time-picker .button{display:inline-flex;cursor:pointer;align-items:center;justify-content:center;background-color:var(--calcite-time-picker-background-color, var(--calcite-color-foreground-1))}.time-picker .button:hover,.time-picker .button:focus{outline:2px solid transparent;outline-offset:2px;z-index:var(--calcite-z-index-header);outline-offset:0;background-color:var(--calcite-time-picker-button-background-color-hover, var(--calcite-color-foreground-2))}.time-picker .button:active{background-color:var(--calcite-time-picker-button-background-color-press, var(--calcite-color-foreground-3))}.time-picker .button.top-left{border-start-start-radius:var(--calcite-time-picker-corner-radius, var(--calcite-border-radius-round))}.time-picker .button.bottom-left{border-end-start-radius:var(--calcite-time-picker-corner-radius, var(--calcite-border-radius-round))}.time-picker .button.top-right{border-start-end-radius:var(--calcite-time-picker-corner-radius, var(--calcite-border-radius-round))}.time-picker .button.bottom-right{border-end-end-radius:var(--calcite-time-picker-corner-radius, var(--calcite-border-radius-round))}.time-picker .button calcite-icon{color:var(--calcite-time-picker-icon-color, var(--calcite-color-text-3))}.time-picker .input{display:inline-flex;cursor:pointer;align-items:center;justify-content:center;font-weight:var(--calcite-font-weight-medium);background-color:var(--calcite-time-picker-background-color, var(--calcite-color-foreground-1))}.time-picker .input:hover{box-shadow:inset 0 0 0 2px var(--calcite-time-picker-input-border-color-hover, var(--calcite-color-foreground-2));z-index:var(--calcite-z-index-header)}.time-picker .input:focus,.time-picker .input:hover:focus{outline:2px solid transparent;outline-offset:2px;outline-offset:0}.time-picker .input.inputFocus,.time-picker .input:hover.inputFocus{box-shadow:inset 0 0 0 2px var(--calcite-time-picker-input-border-color-press, var(--calcite-color-brand));z-index:var(--calcite-z-index-header)}.time-picker.scale-s{font-size:var(--calcite-font-size-relative-base)}.time-picker.scale-s .button,.time-picker.scale-s .input{padding-inline:var(--calcite-spacing-sm);padding-block:var(--calcite-spacing-xxs)}.time-picker.scale-s:not(.show-meridiem) .delimiter:last-child{padding-inline-end:var(--calcite-spacing-md)}.time-picker.scale-m{font-size:var(--calcite-font-size-relative-md)}.time-picker.scale-m .button,.time-picker.scale-m .input{padding-inline:var(--calcite-spacing-md);padding-block:var(--calcite-spacing-sm)}.time-picker.scale-m:not(.show-meridiem) .delimiter:last-child{padding-inline-end:var(--calcite-spacing-xl)}.time-picker.scale-l{font-size:var(--calcite-font-size-relative-lg)}.time-picker.scale-l .button,.time-picker.scale-l .input{padding-inline:var(--calcite-spacing-lg);padding-block:var(--calcite-spacing-md)}.time-picker.scale-l:not(.show-meridiem) .delimiter:last-child{padding-inline-end:var(--calcite-spacing-xxl)}:host([hidden]){display:none}[hidden]{display:none}`;
class TimePicker extends LitElement {
constructor() {
super();
this.fractionalSecondRef = createRef();
this.hourRef = createRef();
this.meridiemRef = createRef();
this.minuteRef = createRef();
this.pointerActivated = false;
this.secondRef = createRef();
this.messages = useT9n();
this.focusSetter = useSetFocus()(this);
this.time = useTime(this);
this.hourFormat = "user";
this.scale = "m";
this.step = 60;
this.value = null;
this.calciteTimePickerChange = createEvent({ cancelable: false });
this.listen("blur", this.blurHandler);
this.listen("calciteTimeChange", this.timeChangeHandler);
this.listen("keydown", this.keyDownHandler);
this.listen("pointerdown", this.pointerDownHandler);
}
static {
this.properties = { activeEl: [16, {}, { state: true }], showFractionalSecond: [16, {}, { state: true }], showSecond: [16, {}, { state: true }], time: [0, {}, { attribute: false }], hourFormat: [3, {}, { reflect: true }], messageOverrides: [0, {}, { attribute: false }], numberingSystem: 1, scale: [3, {}, { reflect: true }], step: [11, {}, { reflect: true, type: Number }], value: 1 };
}
static {
this.shadowRootOptions = { mode: "open", delegatesFocus: true };
}
static {
this.styles = styles;
}
async setFocus(options) {
return this.focusSetter(() => this.el, options);
}
connectedCallback() {
super.connectedCallback();
this.toggleSecond();
}
willUpdate(changes) {
if (changes.has("step") && (this.hasUpdated || this.step !== 60)) {
this.toggleSecond();
}
if (changes.has("value") && (this.hasUpdated || this.value !== null)) {
this.time.setValue(this.value);
}
}
blurHandler() {
this.activeEl = void 0;
this.pointerActivated = false;
}
keyDownHandler(event) {
this.pointerActivated = false;
const { defaultPrevented, key } = event;
if (defaultPrevented) {
return;
}
const { hourFormat } = this.time;
switch (this.activeEl) {
case this.hourRef.value:
if (key === "ArrowRight") {
this.focusPart("minute");
event.preventDefault();
}
break;
case this.minuteRef.value:
switch (key) {
case "ArrowLeft":
this.focusPart("hour");
event.preventDefault();
break;
case "ArrowRight":
if (this.step !== 60) {
this.focusPart("second");
event.preventDefault();
} else if (hourFormat === "12") {
this.focusPart("meridiem");
event.preventDefault();
}
break;
}
break;
case this.secondRef.value:
switch (key) {
case "ArrowLeft":
this.focusPart("minute");
event.preventDefault();
break;
case "ArrowRight":
if (this.showFractionalSecond) {
this.focusPart("fractionalSecond");
} else if (hourFormat === "12") {
this.focusPart("meridiem");
event.preventDefault();
}
break;
}
break;
case this.fractionalSecondRef.value:
switch (key) {
case "ArrowLeft":
this.focusPart("second");
event.preventDefault();
break;
case "ArrowRight":
if (hourFormat === "12") {
this.focusPart("meridiem");
event.preventDefault();
}
break;
}
break;
case this.meridiemRef.value:
switch (key) {
case "ArrowLeft":
if (this.showFractionalSecond) {
this.focusPart("fractionalSecond");
} else if (this.step !== 60) {
this.focusPart("second");
event.preventDefault();
} else {
this.focusPart("minute");
event.preventDefault();
}
break;
}
break;
}
}
pointerDownHandler() {
this.pointerActivated = true;
}
async focusPart(target) {
await componentFocusable(this);
const ref2 = target === "hour" ? this.hourRef : target === "minute" ? this.minuteRef : target === "second" ? this.secondRef : target === "fractionalSecond" ? this.fractionalSecondRef : this.meridiemRef;
ref2.value?.focus();
}
focusHandler(event) {
if (this.pointerActivated) {
return;
}
this.activeEl = event.currentTarget;
}
fractionalSecondDownClickHandler() {
this.activeEl = this.fractionalSecondRef.value;
this.activeEl.focus();
this.time.nudgeFractionalSecond("down");
}
fractionalSecondUpClickHandler() {
this.activeEl = this.fractionalSecondRef.value;
this.activeEl.focus();
this.time.nudgeFractionalSecond("up");
}
hourDownClickHandler() {
this.activeEl = this.hourRef.value;
this.activeEl.focus();
this.time.decrementHour();
}
hourUpClickHandler() {
this.activeEl = this.hourRef.value;
this.activeEl.focus();
this.time.incrementHour();
}
inputClickHandler(event) {
this.activeEl = event.target;
}
meridiemUpClickHandler() {
this.activeEl = this.meridiemRef.value;
this.activeEl.focus();
this.time.toggleMeridiem("up");
}
meridiemDownClickHandler() {
this.activeEl = this.meridiemRef.value;
this.activeEl.focus();
this.time.toggleMeridiem("down");
}
minuteDownClickHandler() {
this.activeEl = this.minuteRef.value;
this.activeEl.focus();
this.time.decrementMinute();
}
minuteUpClickHandler() {
this.activeEl = this.minuteRef.value;
this.activeEl.focus();
this.time.incrementMinute();
}
secondDownClickHandler() {
this.activeEl = this.secondRef.value;
this.activeEl.focus();
this.time.decrementSecond();
}
secondUpClickHandler() {
this.activeEl = this.secondRef.value;
this.activeEl.focus();
this.time.incrementSecond();
}
timeChangeHandler(event) {
event.stopPropagation();
const newValue = event.detail;
if (newValue !== this.value) {
this.value = newValue;
}
this.calciteTimePickerChange.emit();
}
toggleSecond() {
this.showSecond = this.step < 60;
this.stepPrecision = decimalPlaces(this.step);
this.showFractionalSecond = this.stepPrecision > 0;
}
render() {
const { activeEl, messages, scale } = this;
const { _lang: locale } = messages;
const { fractionalSecond, handleFractionalSecondKeyDownEvent, handleHourKeyDownEvent, handleMeridiemKeyDownEvent, handleMinuteKeyDownEvent, handleSecondKeyDownEvent, hour, hourFormat, localizedDecimalSeparator, localizedFractionalSecond, localizedHour, localizedHourSuffix, localizedMeridiem, localizedMinute, localizedMinuteSuffix, localizedSecond, localizedSecondSuffix, meridiem, meridiemOrder, minute, second } = this.time;
const hourIsNumber = isValidNumber(hour);
const iconScale = getIconScale(scale);
const minuteIsNumber = isValidNumber(minute);
const secondIsNumber = isValidNumber(second);
const fractionalSecondIsNumber = isValidNumber(fractionalSecond);
const showSecondSuffix = locale !== "bg" && localizedSecondSuffix;
const showMeridiem = hourFormat === "12";
return html`<div class=${safeClassMap({
[CSS.timePicker]: true,
[CSS.showMeridiem]: showMeridiem,
[CSS.showSecond]: this.showSecond,
[CSS.scale(scale)]: true
})} dir=ltr><div class=${safeClassMap(CSS.column)} role=group><span .ariaLabel=${messages.hourUp} class=${safeClassMap({
[CSS.button]: true,
[CSS.buttonHourUp]: true,
[CSS.buttonTopLeft]: true
})} @click=${this.hourUpClickHandler} role=button><calcite-icon .icon=${ICONS.chevronUp} .scale=${iconScale}></calcite-icon></span><span .ariaLabel=${messages.hour} aria-valuemax=23 aria-valuemin=1 .ariaValueNow=${hourIsNumber && parseInt(hour) || "0"} .ariaValueText=${hour} class=${safeClassMap({
[CSS.input]: true,
[CSS.hour]: true,
[CSS.inputFocus]: activeEl && activeEl === this.hourRef.value
})} @click=${this.inputClickHandler} @focus=${this.focusHandler} @keydown=${handleHourKeyDownEvent} role=spinbutton tabindex=0 ${ref(this.hourRef)}>${localizedHour || "--"}</span><span .ariaLabel=${messages.hourDown} class=${safeClassMap({
[CSS.button]: true,
[CSS.buttonHourDown]: true,
[CSS.buttonBottomLeft]: true
})} @click=${this.hourDownClickHandler} role=button><calcite-icon .icon=${ICONS.chevronDown} .scale=${iconScale}></calcite-icon></span></div><span class=${safeClassMap({ [CSS.delimiter]: true, [CSS.hourSuffix]: true })}>${localizedHourSuffix}</span><div class=${safeClassMap(CSS.column)} role=group><span .ariaLabel=${messages.minuteUp} class=${safeClassMap({
[CSS.button]: true,
[CSS.buttonMinuteUp]: true
})} @click=${this.minuteUpClickHandler} role=button><calcite-icon .icon=${ICONS.chevronUp} .scale=${iconScale}></calcite-icon></span><span .ariaLabel=${messages.minute} aria-valuemax=12 aria-valuemin=1 .ariaValueNow=${minuteIsNumber && parseInt(minute) || "0"} .ariaValueText=${minute} class=${safeClassMap({
[CSS.input]: true,
[CSS.minute]: true,
[CSS.inputFocus]: activeEl && activeEl === this.minuteRef.value
})} @click=${this.inputClickHandler} @focus=${this.focusHandler} @keydown=${handleMinuteKeyDownEvent} role=spinbutton tabindex=0 ${ref(this.minuteRef)}>${localizedMinute || "--"}</span><span .ariaLabel=${messages.minuteDown} class=${safeClassMap({
[CSS.button]: true,
[CSS.buttonMinuteDown]: true
})} @click=${this.minuteDownClickHandler} role=button><calcite-icon .icon=${ICONS.chevronDown} .scale=${iconScale}></calcite-icon></span></div>${this.showSecond && html`<span class=${safeClassMap({ [CSS.delimiter]: true, [CSS.minuteSuffix]: true })}>${localizedMinuteSuffix}</span>` || ""}${this.showSecond && html`<div class=${safeClassMap(CSS.column)} role=group><span .ariaLabel=${messages.secondUp} class=${safeClassMap({
[CSS.button]: true,
[CSS.buttonSecondUp]: true
})} @click=${this.secondUpClickHandler} role=button><calcite-icon .icon=${ICONS.chevronUp} .scale=${iconScale}></calcite-icon></span><span .ariaLabel=${messages.second} aria-valuemax=59 aria-valuemin=0 .ariaValueNow=${secondIsNumber && parseInt(second) || "0"} .ariaValueText=${second} class=${safeClassMap({
[CSS.input]: true,
[CSS.second]: true,
[CSS.inputFocus]: activeEl && activeEl === this.secondRef.value
})} @click=${this.inputClickHandler} @focus=${this.focusHandler} @keydown=${handleSecondKeyDownEvent} role=spinbutton tabindex=0 ${ref(this.secondRef)}>${localizedSecond || "--"}</span><span .ariaLabel=${messages.secondDown} class=${safeClassMap({
[CSS.button]: true,
[CSS.buttonSecondDown]: true
})} @click=${this.secondDownClickHandler} role=button><calcite-icon .icon=${ICONS.chevronDown} .scale=${iconScale}></calcite-icon></span></div>` || ""}${this.showFractionalSecond && html`<span class=${safeClassMap({ [CSS.delimiter]: true, [CSS.decimalSeparator]: true })}>${localizedDecimalSeparator}</span>` || ""}${this.showFractionalSecond && html`<div class=${safeClassMap(CSS.column)} role=group><span .ariaLabel=${messages.fractionalSecondUp} class=${safeClassMap({
[CSS.button]: true,
[CSS.buttonFractionalSecondUp]: true
})} @click=${this.fractionalSecondUpClickHandler} role=button><calcite-icon .icon=${ICONS.chevronUp} .scale=${iconScale}></calcite-icon></span><span .ariaLabel=${messages.fractionalSecond} aria-valuemax=999 aria-valuemin=1 .ariaValueNow=${fractionalSecondIsNumber && parseInt(fractionalSecond) || "0"} .ariaValueText=${localizedFractionalSecond} class=${safeClassMap({
[CSS.input]: true,
[CSS.fractionalSecond]: true,
[CSS.inputFocus]: activeEl && activeEl === this.fractionalSecondRef.value
})} @click=${this.inputClickHandler} @focus=${this.focusHandler} @keydown=${handleFractionalSecondKeyDownEvent} role=spinbutton tabindex=0 ${ref(this.fractionalSecondRef)}>${localizedFractionalSecond || "".padStart(this.stepPrecision, "-")}</span><span .ariaLabel=${messages.fractionalSecondDown} class=${safeClassMap({
[CSS.button]: true,
[CSS.buttonFractionalSecondDown]: true
})} @click=${this.fractionalSecondDownClickHandler} role=button><calcite-icon .icon=${ICONS.chevronDown} .scale=${iconScale}></calcite-icon></span></div>` || ""}${showSecondSuffix && html`<span class=${safeClassMap({ [CSS.delimiter]: true, [CSS.secondSuffix]: true })}>${localizedSecondSuffix.trim()}</span>` || ""}${showMeridiem && html`<div class=${safeClassMap({
[CSS.column]: true,
[CSS.meridiemStart]: meridiemOrder === 0 || getElementDir(this.el) === "rtl"
})} role=group><span .ariaLabel=${messages.meridiemUp} class=${safeClassMap({
[CSS.button]: true,
[CSS.buttonMeridiemUp]: true,
[CSS.buttonTopRight]: true
})} @click=${this.meridiemUpClickHandler} role=button><calcite-icon .icon=${ICONS.chevronUp} .scale=${iconScale}></calcite-icon></span><span .ariaLabel=${messages.meridiem} aria-valuemax=2 aria-valuemin=1 .ariaValueNow=${meridiem === "PM" && "2" || "1"} .ariaValueText=${meridiem} class=${safeClassMap({
[CSS.input]: true,
[CSS.meridiem]: true,
[CSS.inputFocus]: activeEl && activeEl === this.meridiemRef.value
})} @click=${this.inputClickHandler} @focus=${this.focusHandler} @keydown=${handleMeridiemKeyDownEvent} role=spinbutton tabindex=0 ${ref(this.meridiemRef)}>${localizedMeridiem || "--"}</span><span .ariaLabel=${messages.meridiemDown} class=${safeClassMap({
[CSS.button]: true,
[CSS.buttonMeridiemDown]: true,
[CSS.buttonBottomRight]: true
})} @click=${this.meridiemDownClickHandler} role=button><calcite-icon .icon=${ICONS.chevronDown} .scale=${iconScale}></calcite-icon></span></div>` || ""}</div>`;
}
}
customElement("calcite-time-picker", TimePicker);
export {
TimePicker
};