diff --git a/packages/calcite-components/src/components.d.ts b/packages/calcite-components/src/components.d.ts index f1aa432bc3b..70288fce667 100644 --- a/packages/calcite-components/src/components.d.ts +++ b/packages/calcite-components/src/components.d.ts @@ -9,91 +9,191 @@ import { Alignment, Appearance, CollapseDirection, FlipContext, IconType, Intera import { RequestedItem } from "./components/accordion/interfaces"; import { IconNameOrString } from "./components/icon/interfaces"; import { RequestedItem as RequestedItem1 } from "./components/accordion-item/interfaces"; +import { ActionMessages } from "./components/action/assets/action/t9n"; import { FlipPlacement, LogicalPlacement, MenuPlacement, OverlayPositioning, ReferenceElement } from "./utils/floating-ui"; +import { ActionBarMessages } from "./components/action-bar/assets/action-bar/t9n"; import { Columns } from "./components/action-group/interfaces"; +import { ActionGroupMessages } from "./components/action-group/assets/action-group/t9n"; +import { ActionPadMessages } from "./components/action-pad/assets/action-pad/t9n"; import { AlertDuration, AlertQueue } from "./components/alert/interfaces"; import { NumberingSystem } from "./utils/locale"; +import { AlertMessages } from "./components/alert/assets/alert/t9n"; import { HeadingLevel } from "./components/functional/Heading"; +import { BlockMessages } from "./components/block/assets/block/t9n"; import { BlockSectionToggleDisplay } from "./components/block-section/interfaces"; +import { BlockSectionMessages } from "./components/block-section/assets/block-section/t9n"; import { ButtonAlignment, DropdownIconType } from "./components/button/interfaces"; +import { ButtonMessages } from "./components/button/assets/button/t9n"; +import { CardMessages } from "./components/card/assets/card/t9n"; import { ArrowType, AutoplayType } from "./components/carousel/interfaces"; +import { CarouselMessages } from "./components/carousel/assets/carousel/t9n"; import { MutableValidityState } from "./utils/form"; +import { ChipMessages } from "./components/chip/assets/chip/t9n"; import { ColorValue, InternalColor } from "./components/color-picker/interfaces"; import { Format } from "./components/color-picker/utils"; +import { ColorPickerMessages } from "./components/color-picker/assets/color-picker/t9n"; import { ComboboxChildElement, SelectionDisplay } from "./components/combobox/interfaces"; -import { DateLocaleData } from "./components/date-picker/utils"; +import { ComboboxMessages } from "./components/combobox/assets/combobox/t9n"; +import { DatePickerMessages } from "./components/date-picker/assets/date-picker/t9n"; import { HoverRange } from "./utils/date"; +import { DateLocaleData } from "./components/date-picker/utils"; +import { DialogMessages } from "./components/dialog/assets/dialog/t9n"; import { DialogPlacement } from "./components/dialog/interfaces"; import { RequestedItem as RequestedItem2 } from "./components/dropdown-group/interfaces"; import { ItemKeyboardEvent } from "./components/dropdown/interfaces"; +import { FilterMessages } from "./components/filter/assets/filter/t9n"; import { FlowItemLikeElement } from "./components/flow/interfaces"; +import { FlowItemMessages } from "./components/flow-item/assets/flow-item/t9n"; import { ColorStop, DataSeries } from "./components/graph/interfaces"; +import { HandleMessages } from "./components/handle/assets/handle/t9n"; import { HandleChange, HandleNudge } from "./components/handle/interfaces"; +import { InlineEditableMessages } from "./components/inline-editable/assets/inline-editable/t9n"; import { InputPlacement } from "./components/input/interfaces"; +import { InputMessages } from "./components/input/assets/input/t9n"; +import { InputDatePickerMessages } from "./components/input-date-picker/assets/input-date-picker/t9n"; +import { InputNumberMessages } from "./components/input-number/assets/input-number/t9n"; +import { InputTextMessages } from "./components/input-text/assets/input-text/t9n"; +import { InputTimePickerMessages } from "./components/input-time-picker/assets/input-time-picker/t9n"; +import { TimePickerMessages } from "./components/time-picker/assets/time-picker/t9n"; +import { InputTimeZoneMessages } from "./components/input-time-zone/assets/input-time-zone/t9n"; import { OffsetStyle, TimeZoneMode } from "./components/input-time-zone/interfaces"; import { ListDragDetail } from "./components/list/interfaces"; import { ItemData } from "./components/list-item/interfaces"; +import { ListMessages } from "./components/list/assets/list/t9n"; import { SelectionAppearance } from "./components/list/resources"; +import { ListItemMessages } from "./components/list-item/assets/list-item/t9n"; +import { MenuMessages } from "./components/menu/assets/menu/t9n"; +import { MenuItemMessages } from "./components/menu-item/assets/menu-item/t9n"; import { MenuItemCustomEvent } from "./components/menu-item/interfaces"; import { MeterFillType, MeterLabelType } from "./components/meter/interfaces"; +import { ModalMessages } from "./components/modal/assets/modal/t9n"; +import { NoticeMessages } from "./components/notice/assets/notice/t9n"; +import { PaginationMessages } from "./components/pagination/assets/pagination/t9n"; +import { PanelMessages } from "./components/panel/assets/panel/t9n"; import { ItemData as ItemData1, ListFocusId } from "./components/pick-list/shared-list-logic"; import { ICON_TYPES } from "./components/pick-list/resources"; +import { PickListItemMessages } from "./components/pick-list-item/assets/pick-list-item/t9n"; +import { PopoverMessages } from "./components/popover/assets/popover/t9n"; +import { RatingMessages } from "./components/rating/assets/rating/t9n"; +import { ScrimMessages } from "./components/scrim/assets/scrim/t9n"; import { DisplayMode } from "./components/sheet/interfaces"; import { DisplayMode as DisplayMode1 } from "./components/shell-panel/interfaces"; +import { ShellPanelMessages } from "./components/shell-panel/assets/shell-panel/t9n"; import { DragDetail } from "./utils/sortableComponent"; import { StepperItemChangeEventDetail, StepperItemEventDetail, StepperItemKeyEventDetail, StepperLayout } from "./components/stepper/interfaces"; +import { StepperMessages } from "./components/stepper/assets/stepper/t9n"; +import { StepperItemMessages } from "./components/stepper-item/assets/stepper-item/t9n"; import { TabID, TabLayout, TabPosition } from "./components/tabs/interfaces"; +import { TabNavMessages } from "./components/tab-nav/assets/tab-nav/t9n"; import { Element } from "@stencil/core"; import { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces"; +import { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n"; import { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent, TableSelectionDisplay } from "./components/table/interfaces"; +import { TableMessages } from "./components/table/assets/table/t9n"; +import { TableCellMessages } from "./components/table-cell/assets/table-cell/t9n"; +import { TableHeaderMessages } from "./components/table-header/assets/table-header/t9n"; +import { TextAreaMessages } from "./components/text-area/assets/text-area/t9n"; import { TileSelectType } from "./components/tile-select/interfaces"; import { TileSelectGroupLayout } from "./components/tile-select-group/interfaces"; +import { TipMessages } from "./components/tip/assets/tip/t9n"; +import { TipManagerMessages } from "./components/tip-manager/assets/tip-manager/t9n"; import { TreeItemSelectDetail } from "./components/tree-item/interfaces"; +import { ValueListMessages } from "./components/value-list/assets/value-list/t9n"; import { ListItemAndHandle } from "./components/value-list-item/interfaces"; export { Alignment, Appearance, CollapseDirection, FlipContext, IconType, InteractionMode, Kind, Layout, LogicalFlowPosition, Position, Scale, SelectionAppearance as SelectionAppearance1, SelectionMode, Status, Width } from "./components/interfaces"; export { RequestedItem } from "./components/accordion/interfaces"; export { IconNameOrString } from "./components/icon/interfaces"; export { RequestedItem as RequestedItem1 } from "./components/accordion-item/interfaces"; +export { ActionMessages } from "./components/action/assets/action/t9n"; export { FlipPlacement, LogicalPlacement, MenuPlacement, OverlayPositioning, ReferenceElement } from "./utils/floating-ui"; +export { ActionBarMessages } from "./components/action-bar/assets/action-bar/t9n"; export { Columns } from "./components/action-group/interfaces"; +export { ActionGroupMessages } from "./components/action-group/assets/action-group/t9n"; +export { ActionPadMessages } from "./components/action-pad/assets/action-pad/t9n"; export { AlertDuration, AlertQueue } from "./components/alert/interfaces"; export { NumberingSystem } from "./utils/locale"; +export { AlertMessages } from "./components/alert/assets/alert/t9n"; export { HeadingLevel } from "./components/functional/Heading"; +export { BlockMessages } from "./components/block/assets/block/t9n"; export { BlockSectionToggleDisplay } from "./components/block-section/interfaces"; +export { BlockSectionMessages } from "./components/block-section/assets/block-section/t9n"; export { ButtonAlignment, DropdownIconType } from "./components/button/interfaces"; +export { ButtonMessages } from "./components/button/assets/button/t9n"; +export { CardMessages } from "./components/card/assets/card/t9n"; export { ArrowType, AutoplayType } from "./components/carousel/interfaces"; +export { CarouselMessages } from "./components/carousel/assets/carousel/t9n"; export { MutableValidityState } from "./utils/form"; +export { ChipMessages } from "./components/chip/assets/chip/t9n"; export { ColorValue, InternalColor } from "./components/color-picker/interfaces"; export { Format } from "./components/color-picker/utils"; +export { ColorPickerMessages } from "./components/color-picker/assets/color-picker/t9n"; export { ComboboxChildElement, SelectionDisplay } from "./components/combobox/interfaces"; -export { DateLocaleData } from "./components/date-picker/utils"; +export { ComboboxMessages } from "./components/combobox/assets/combobox/t9n"; +export { DatePickerMessages } from "./components/date-picker/assets/date-picker/t9n"; export { HoverRange } from "./utils/date"; +export { DateLocaleData } from "./components/date-picker/utils"; +export { DialogMessages } from "./components/dialog/assets/dialog/t9n"; export { DialogPlacement } from "./components/dialog/interfaces"; export { RequestedItem as RequestedItem2 } from "./components/dropdown-group/interfaces"; export { ItemKeyboardEvent } from "./components/dropdown/interfaces"; +export { FilterMessages } from "./components/filter/assets/filter/t9n"; export { FlowItemLikeElement } from "./components/flow/interfaces"; +export { FlowItemMessages } from "./components/flow-item/assets/flow-item/t9n"; export { ColorStop, DataSeries } from "./components/graph/interfaces"; +export { HandleMessages } from "./components/handle/assets/handle/t9n"; export { HandleChange, HandleNudge } from "./components/handle/interfaces"; +export { InlineEditableMessages } from "./components/inline-editable/assets/inline-editable/t9n"; export { InputPlacement } from "./components/input/interfaces"; +export { InputMessages } from "./components/input/assets/input/t9n"; +export { InputDatePickerMessages } from "./components/input-date-picker/assets/input-date-picker/t9n"; +export { InputNumberMessages } from "./components/input-number/assets/input-number/t9n"; +export { InputTextMessages } from "./components/input-text/assets/input-text/t9n"; +export { InputTimePickerMessages } from "./components/input-time-picker/assets/input-time-picker/t9n"; +export { TimePickerMessages } from "./components/time-picker/assets/time-picker/t9n"; +export { InputTimeZoneMessages } from "./components/input-time-zone/assets/input-time-zone/t9n"; export { OffsetStyle, TimeZoneMode } from "./components/input-time-zone/interfaces"; export { ListDragDetail } from "./components/list/interfaces"; export { ItemData } from "./components/list-item/interfaces"; +export { ListMessages } from "./components/list/assets/list/t9n"; export { SelectionAppearance } from "./components/list/resources"; +export { ListItemMessages } from "./components/list-item/assets/list-item/t9n"; +export { MenuMessages } from "./components/menu/assets/menu/t9n"; +export { MenuItemMessages } from "./components/menu-item/assets/menu-item/t9n"; export { MenuItemCustomEvent } from "./components/menu-item/interfaces"; export { MeterFillType, MeterLabelType } from "./components/meter/interfaces"; +export { ModalMessages } from "./components/modal/assets/modal/t9n"; +export { NoticeMessages } from "./components/notice/assets/notice/t9n"; +export { PaginationMessages } from "./components/pagination/assets/pagination/t9n"; +export { PanelMessages } from "./components/panel/assets/panel/t9n"; export { ItemData as ItemData1, ListFocusId } from "./components/pick-list/shared-list-logic"; export { ICON_TYPES } from "./components/pick-list/resources"; +export { PickListItemMessages } from "./components/pick-list-item/assets/pick-list-item/t9n"; +export { PopoverMessages } from "./components/popover/assets/popover/t9n"; +export { RatingMessages } from "./components/rating/assets/rating/t9n"; +export { ScrimMessages } from "./components/scrim/assets/scrim/t9n"; export { DisplayMode } from "./components/sheet/interfaces"; export { DisplayMode as DisplayMode1 } from "./components/shell-panel/interfaces"; +export { ShellPanelMessages } from "./components/shell-panel/assets/shell-panel/t9n"; export { DragDetail } from "./utils/sortableComponent"; export { StepperItemChangeEventDetail, StepperItemEventDetail, StepperItemKeyEventDetail, StepperLayout } from "./components/stepper/interfaces"; +export { StepperMessages } from "./components/stepper/assets/stepper/t9n"; +export { StepperItemMessages } from "./components/stepper-item/assets/stepper-item/t9n"; export { TabID, TabLayout, TabPosition } from "./components/tabs/interfaces"; +export { TabNavMessages } from "./components/tab-nav/assets/tab-nav/t9n"; export { Element } from "@stencil/core"; export { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces"; +export { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n"; export { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent, TableSelectionDisplay } from "./components/table/interfaces"; +export { TableMessages } from "./components/table/assets/table/t9n"; +export { TableCellMessages } from "./components/table-cell/assets/table-cell/t9n"; +export { TableHeaderMessages } from "./components/table-header/assets/table-header/t9n"; +export { TextAreaMessages } from "./components/text-area/assets/text-area/t9n"; export { TileSelectType } from "./components/tile-select/interfaces"; export { TileSelectGroupLayout } from "./components/tile-select-group/interfaces"; +export { TipMessages } from "./components/tip/assets/tip/t9n"; +export { TipManagerMessages } from "./components/tip-manager/assets/tip-manager/t9n"; export { TreeItemSelectDetail } from "./components/tree-item/interfaces"; +export { ValueListMessages } from "./components/value-list/assets/value-list/t9n"; export { ListItemAndHandle } from "./components/value-list-item/interfaces"; export namespace Components { interface CalciteAccordion { @@ -1402,6 +1502,10 @@ export namespace Components { * Specifies the heading level of the component's `heading` for proper document structure, without affecting visual styling. */ "headingLevel": HeadingLevel; + /** + * Defines the layout of the component. + */ + "layout": "horizontal" | "vertical"; /** * Specifies the latest allowed date (`"yyyy-mm-dd"`). */ @@ -1426,6 +1530,10 @@ export namespace Components { * Specifies the earliest allowed date as a full date object (`new Date("yyyy-mm-dd")`). */ "minAsDate": Date; + /** + * Specifies the monthStyle used by the component. + */ + "monthStyle": "abbreviated" | "wide"; /** * Specifies the Unicode numeral system used by the component for localization. This property cannot be dynamically changed. */ @@ -1534,10 +1642,18 @@ export namespace Components { * End date currently active. */ "endDate"?: Date; + /** + * Specifies the number at which section headings should start. + */ + "headingLevel": HeadingLevel; /** * The range of dates currently being hovered. */ "hoverRange": HoverRange; + /** + * Specifies the layout of the component. + */ + "layout": "horizontal" | "vertical"; /** * CLDR locale data for current locale. */ @@ -1546,10 +1662,22 @@ export namespace Components { * Specifies the latest allowed date (`"yyyy-mm-dd"`). */ "max": Date; + /** + * Made into a prop for testing purposes only + */ + "messages": DatePickerMessages; /** * Specifies the earliest allowed date (`"yyyy-mm-dd"`). */ "min": Date; + /** + * Specifies the monthStyle used by the component. + */ + "monthStyle": "abbreviated" | "wide"; + /** + * When `true`, activates the component's range mode which renders two calendars for selecting ranges of dates. + */ + "range": boolean; /** * Specifies the size of the component. */ @@ -1589,6 +1717,14 @@ export namespace Components { * Specifies the earliest allowed date (`"yyyy-mm-dd"`). */ "min": Date; + /** + * Specifies the monthStyle used by the component. + */ + "monthStyle": "abbreviated" | "wide"; + /** + * Specifies the position of the component in a range date-picker. + */ + "position": Extract<"start" | "end", Position>; /** * Specifies the size of the component. */ @@ -2389,6 +2525,10 @@ export namespace Components { * Specifies the earliest allowed date as a full date object. */ "minAsDate": Date; + /** + * Specifies the monthStyle used by the component. + */ + "monthStyle": "abbreviated" | "wide"; /** * Specifies the name of the component. Required to pass the component's `value` on form submission. */ @@ -6603,7 +6743,7 @@ declare global { new (): HTMLCalciteDatePickerElement; }; interface HTMLCalciteDatePickerDayElementEventMap { - "calciteDaySelect": void; + "calciteInternalDaySelect": void; "calciteInternalDayHover": void; } interface HTMLCalciteDatePickerDayElement extends Components.CalciteDatePickerDay, HTMLStencilElement { @@ -6621,10 +6761,14 @@ declare global { new (): HTMLCalciteDatePickerDayElement; }; interface HTMLCalciteDatePickerMonthElementEventMap { - "calciteInternalDatePickerSelect": Date; - "calciteInternalDatePickerHover": Date; - "calciteInternalDatePickerActiveDateChange": Date; - "calciteInternalDatePickerMouseOut": void; + "calciteInternalDatePickerDaySelect": Date; + "calciteInternalDatePickerDayHover": Date; + "calciteInternalDatePickerMonthActiveDateChange": Date; + "calciteInternalDatePickerMonthMouseOut": void; + "calciteInternalDatePickerMonthChange": { + date: Date; + position: string; + }; } interface HTMLCalciteDatePickerMonthElement extends Components.CalciteDatePickerMonth, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLCalciteDatePickerMonthElement, ev: CalciteDatePickerMonthCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -6641,7 +6785,7 @@ declare global { new (): HTMLCalciteDatePickerMonthElement; }; interface HTMLCalciteDatePickerMonthHeaderElementEventMap { - "calciteInternalDatePickerSelect": Date; + "calciteInternalDatePickerMonthHeaderSelectChange": Date; } interface HTMLCalciteDatePickerMonthHeaderElement extends Components.CalciteDatePickerMonthHeader, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLCalciteDatePickerMonthHeaderElement, ev: CalciteDatePickerMonthHeaderCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -9449,6 +9593,10 @@ declare namespace LocalJSX { * Specifies the heading level of the component's `heading` for proper document structure, without affecting visual styling. */ "headingLevel"?: HeadingLevel; + /** + * Defines the layout of the component. + */ + "layout"?: "horizontal" | "vertical"; /** * Specifies the latest allowed date (`"yyyy-mm-dd"`). */ @@ -9473,6 +9621,10 @@ declare namespace LocalJSX { * Specifies the earliest allowed date as a full date object (`new Date("yyyy-mm-dd")`). */ "minAsDate"?: Date; + /** + * Specifies the monthStyle used by the component. + */ + "monthStyle"?: "abbreviated" | "wide"; /** * Specifies the Unicode numeral system used by the component for localization. This property cannot be dynamically changed. */ @@ -9535,14 +9687,14 @@ declare namespace LocalJSX { * Date is currently highlighted as part of the range, */ "highlighted"?: boolean; - /** - * Fires when user selects day. - */ - "onCalciteDaySelect"?: (event: CalciteDatePickerDayCustomEvent) => void; /** * Fires when user hovers over a day. */ "onCalciteInternalDayHover"?: (event: CalciteDatePickerDayCustomEvent) => void; + /** + * Fires when user selects day. + */ + "onCalciteInternalDaySelect"?: (event: CalciteDatePickerDayCustomEvent) => void; /** * When `true`, activates the component's range mode to allow a start and end date. */ @@ -9585,10 +9737,18 @@ declare namespace LocalJSX { * End date currently active. */ "endDate"?: Date; + /** + * Specifies the number at which section headings should start. + */ + "headingLevel"?: HeadingLevel; /** * The range of dates currently being hovered. */ "hoverRange"?: HoverRange; + /** + * Specifies the layout of the component. + */ + "layout"?: "horizontal" | "vertical"; /** * CLDR locale data for current locale. */ @@ -9597,23 +9757,42 @@ declare namespace LocalJSX { * Specifies the latest allowed date (`"yyyy-mm-dd"`). */ "max"?: Date; + /** + * Made into a prop for testing purposes only + */ + "messages"?: DatePickerMessages; /** * Specifies the earliest allowed date (`"yyyy-mm-dd"`). */ "min"?: Date; /** - * Active date for the user keyboard access. + * Specifies the monthStyle used by the component. */ - "onCalciteInternalDatePickerActiveDateChange"?: (event: CalciteDatePickerMonthCustomEvent) => void; + "monthStyle"?: "abbreviated" | "wide"; /** * Fires when user hovers the date. */ - "onCalciteInternalDatePickerHover"?: (event: CalciteDatePickerMonthCustomEvent) => void; - "onCalciteInternalDatePickerMouseOut"?: (event: CalciteDatePickerMonthCustomEvent) => void; + "onCalciteInternalDatePickerDayHover"?: (event: CalciteDatePickerMonthCustomEvent) => void; /** * Fires when user selects the date. */ - "onCalciteInternalDatePickerSelect"?: (event: CalciteDatePickerMonthCustomEvent) => void; + "onCalciteInternalDatePickerDaySelect"?: (event: CalciteDatePickerMonthCustomEvent) => void; + /** + * Active date for the user keyboard access. + */ + "onCalciteInternalDatePickerMonthActiveDateChange"?: (event: CalciteDatePickerMonthCustomEvent) => void; + /** + * Emits when user updates month or year using `calcite-date-picker-month-header` component. + */ + "onCalciteInternalDatePickerMonthChange"?: (event: CalciteDatePickerMonthCustomEvent<{ + date: Date; + position: string; + }>) => void; + "onCalciteInternalDatePickerMonthMouseOut"?: (event: CalciteDatePickerMonthCustomEvent) => void; + /** + * When `true`, activates the component's range mode which renders two calendars for selecting ranges of dates. + */ + "range"?: boolean; /** * Specifies the size of the component. */ @@ -9653,10 +9832,18 @@ declare namespace LocalJSX { * Specifies the earliest allowed date (`"yyyy-mm-dd"`). */ "min"?: Date; + /** + * Specifies the monthStyle used by the component. + */ + "monthStyle"?: "abbreviated" | "wide"; /** * Fires to active date */ - "onCalciteInternalDatePickerSelect"?: (event: CalciteDatePickerMonthHeaderCustomEvent) => void; + "onCalciteInternalDatePickerMonthHeaderSelectChange"?: (event: CalciteDatePickerMonthHeaderCustomEvent) => void; + /** + * Specifies the position of the component in a range date-picker. + */ + "position"?: Extract<"start" | "end", Position>; /** * Specifies the size of the component. */ @@ -10477,6 +10664,10 @@ declare namespace LocalJSX { * Specifies the earliest allowed date as a full date object. */ "minAsDate"?: Date; + /** + * Specifies the monthStyle used by the component. + */ + "monthStyle"?: "abbreviated" | "wide"; /** * Specifies the name of the component. Required to pass the component's `value` on form submission. */ diff --git a/packages/calcite-components/src/components/action/action.scss b/packages/calcite-components/src/components/action/action.scss index 1966054df1d..716007fec7b 100755 --- a/packages/calcite-components/src/components/action/action.scss +++ b/packages/calcite-components/src/components/action/action.scss @@ -173,7 +173,8 @@ button { :host([scale="s"]) { .button { - @apply text-n2h px-2 py-1 font-normal; + @apply text-n2h px-2 font-normal; + padding-block: var(--calcite-internal-action-padding-block, var(--calcite-size-xxs)); } .button--text-visible .icon-container { margin-inline-end: theme("spacing.2"); @@ -182,7 +183,8 @@ button { :host([scale="m"]) { .button { - @apply text-n1h px-4 py-3 font-normal; + @apply text-n1h px-4 font-normal; + padding-block: var(--calcite-internal-action-padding-block, var(--calcite-size-md)); } .button--text-visible .icon-container { margin-inline-end: theme("spacing.3"); @@ -191,7 +193,8 @@ button { :host([scale="l"]) { .button { - @apply text-0h p-5 font-normal; + @apply text-0h px-5 font-normal; + padding-block: var(--calcite-internal-action-padding-block, var(--calcite-size-xl)); } .button--text-visible .icon-container { margin-inline-end: theme("spacing.4"); diff --git a/packages/calcite-components/src/components/date-picker-day/date-picker-day.scss b/packages/calcite-components/src/components/date-picker-day/date-picker-day.scss index ad0daa2ff6b..1bccad93d51 100644 --- a/packages/calcite-components/src/components/date-picker-day/date-picker-day.scss +++ b/packages/calcite-components/src/components/date-picker-day/date-picker-day.scss @@ -1,88 +1,33 @@ :host { @apply cursor-pointer flex relative text-color-3; + outline: none; } @include disabled(); -@mixin range-part-base() { - &::before, - &::after { - @apply absolute pointer-events-none; - inset-block: 0; - content: ""; - block-size: var(--calcite-internal-day-size); - inline-size: var(--calcite-internal-day-size); - } -} - -@mixin range-part-edge-end() { - &::before { - inset-inline-end: 50%; - } - &::after { - inset-inline-start: 50%; - border-start-end-radius: var(--calcite-internal-day-size); - border-end-end-radius: var(--calcite-internal-day-size); - inline-size: calc(var(--calcite-internal-day-size) / 2); - } -} - -@mixin range-part-edge-start() { - &::before { - inset-inline-end: 50%; - border-start-start-radius: var(--calcite-internal-day-size); - border-end-start-radius: var(--calcite-internal-day-size); - inline-size: calc(var(--calcite-internal-day-size) / 2); - } - &::after { - inset-inline-start: 50%; - } -} - -@mixin range-part-middle() { - &::before { - inset-inline-end: 50%; - border-radius: 0; - } - &::after { - inset-inline-start: 50%; - border-radius: 0; - } -} - -.day-v-wrapper { - @apply flex-auto; -} - .day-wrapper { @apply flex flex-col items-center - relative; -} - -:host([range]), -:host([range-hover]) { - .day-wrapper { - @include range-part-base(); - } + justify-center + relative + w-full; } .day { @apply text-n2h text-color-3 flex - focus-base items-center justify-center - rounded-full leading-none transition-default - z-default; + w-full + relative; + line-height: var(--calcite-font-line-height-fixed-base); background: none; - box-shadow: 0 0 0 2px transparent; block-size: var(--calcite-internal-day-size); - inline-size: var(--calcite-internal-day-size); + outline-color: var(--calcite-color-transparent); } .text { @@ -91,42 +36,22 @@ } :host([scale="s"]) { - --calcite-internal-day-size: 27px; - - .day-v-wrapper { - @apply py-0.5; - } - .day-wrapper { - @apply p-0; - } + --calcite-internal-day-size: #{$calcite-size-32}; .day { @apply text-n2; } } :host([scale="m"]) { - --calcite-internal-day-size: 33px; - - .day-v-wrapper { - @apply py-1; - } - .day-wrapper { - @apply p-0; - } + --calcite-internal-day-size: #{$calcite-size-40}; .day { @apply text-n1; } } :host([scale="l"]) { - --calcite-internal-day-size: 43px; + --calcite-internal-day-size: #{$calcite-size-44}; - .day-v-wrapper { - @apply py-1; - } - .day-wrapper { - @apply px-1; - } .day { @apply text-0; } @@ -136,20 +61,26 @@ @apply opacity-disabled; } -:host(:hover:not([disabled]):not([selected])), -:host([active]:not([range]):not([selected])) { +:host(:hover:not([disabled]):not([selected])) { & .day { @apply bg-foreground-2 text-color-1; } } -:host(:focus), -:host([active]) { - @apply outline-none; +:host(:not([range]):not([selected]).current-day) { + & .day { + color: var(--calcite-color-text-1); + font-weight: var(--calcite-font-weight-medium); + } +} + +:host(:focus[selected]) .day { + @apply focus-outset z-default; + box-shadow: 0 0 0 2px var(--calcite-color-foreground-1); } -:host(:focus:not([disabled])) .day { - @apply focus-outset; +:host(:focus:not([disabled]):not([selected])) .day { + @apply focus-inset; } :host([selected]) .day { @@ -158,83 +89,31 @@ color: var(--calcite-color-foreground-1); } -:host(:focus:not([disabled])), -:host([start-of-range]:not(:focus)), -:host([end-of-range]:not(:focus)) { +:host([range-hover]:not([selected])) { .day { - box-shadow: 0 0 0 2px var(--calcite-color-foreground-1); + @apply bg-foreground-2; + @apply text-color-1; } } -:host([range-hover]:not([selected])), :host([highlighted]:not([selected])) { - .day-wrapper { - @include range-part-middle(); - } - .day { - @apply text-color-1; + color: var(--calcite-color-brand); + background-color: var(--calcite-color-foreground-current); } } -:host([highlighted]), -:host([selected]:not(.hover--outside-range)) { - .day-wrapper { - &::before, - &::after { - background-color: var(--calcite-color-foreground-current); - } - } -} - -:host([range-hover]:not([selected])) { - .day-wrapper { - &::before, - &::after { - @apply bg-foreground-2; - } - } -} - -:host(:hover[range-hover]:not([selected]).focused--end), -:host([highlighted][end-of-range]), -:host([highlighted][range-edge="end"]), -:host([range-hover][range-edge="end"]), -:host(:hover[range-hover].focused--end.hover--outside-range) { - .day-wrapper { - @include range-part-edge-end(); - } -} - -:host([highlighted][start-of-range]), -:host([highlighted][range-edge="start"]), -:host([range-hover][range-edge="start"]), -:host(:hover[range-hover]:not([selected]).focused--start), -:host([start-of-range].hover--inside-range), -:host(:hover[range-hover].focused--start.hover--outside-range) { - .day-wrapper { - @include range-part-edge-start(); - } -} - -:host([range-hover][start-of-range][range-edge="end"]), -:host([range-hover][end-of-range][range-edge="start"]), -:host([start-of-range][range-edge="end"].hover--inside-range), -:host([end-of-range]) { - .day-wrapper { - &::after, - &::before { - content: unset; - } +:host(:hover[highlighted]:not([selected]).inside-range--hover) { + .day { + background-color: var(--calcite-color-foreground-current); + color: var(--calcite-color-brand); + @apply focus-inset; } } -:host(:hover[range-hover]:not([selected]).focused--start), -:host(:hover[range-hover]:not([selected]).focused--end), -:host(:hover[range-hover]:not([selected]).focused--start.hover--outside-range), -:host(:hover[range-hover]:not([selected]).focused--end.hover--outside-range) { +:host(:hover:not([highlighted]):not([selected]).outside-range--hover) { .day { - box-shadow: 0 0 0 2px var(--calcite-color-foreground-1); + @apply focus-inset; } } @@ -256,22 +135,16 @@ :host([range][selected]), :host([highlighted]), :host([range-hover]:not([selected])) { - .day-wrapper { - &::before, - &::after { - background-color: highlight; - } + .day { + background-color: highlight; } } :host([range-hover]), :host([range][selected][start-of-range]), :host([range][selected][end-of-range]) { - .day-wrapper { - &::before, - &::after { - background-color: canvas; - } + .day { + background-color: canvas; } } } diff --git a/packages/calcite-components/src/components/date-picker-day/date-picker-day.tsx b/packages/calcite-components/src/components/date-picker-day/date-picker-day.tsx index 9c3e465c6d6..065a2098ba7 100644 --- a/packages/calcite-components/src/components/date-picker-day/date-picker-day.tsx +++ b/packages/calcite-components/src/components/date-picker-day/date-picker-day.tsx @@ -100,12 +100,12 @@ export class DatePickerDay implements InteractiveComponent, LoadableComponent { return; } - this.calciteDaySelect.emit(); + this.calciteInternalDaySelect.emit(); }; keyDownHandler = (event: KeyboardEvent): void => { if (isActivationKey(event.key)) { - !this.disabled && this.calciteDaySelect.emit(); + !this.disabled && this.calciteInternalDaySelect.emit(); event.preventDefault(); } }; @@ -128,7 +128,7 @@ export class DatePickerDay implements InteractiveComponent, LoadableComponent { /** * Fires when user selects day. */ - @Event({ cancelable: false }) calciteDaySelect: EventEmitter; + @Event({ cancelable: false }) calciteInternalDaySelect: EventEmitter; /** * Fires when user hovers over a day. @@ -190,12 +190,10 @@ export class DatePickerDay implements InteractiveComponent, LoadableComponent { tabIndex={this.active && !this.disabled ? 0 : -1} > - + )} +
- - -
- - {localizedMonth} - - - (this.yearInput = el)} - type="text" - value={localizedYear} - /> - {suffix && {suffix}} - + {this.renderMonthYearContainer(reverse)} +
+ {!this.position && ( +
{this.renderChevron("left")}
+ )} +
+ {this.position !== "start" && this.renderChevron("right")}
- - - ); } + private renderMonthYearContainer(reverse: boolean): VNode { + const content = reverse + ? [this.renderYearInput(), this.renderMonthPicker()] + : [this.renderMonthPicker(), this.renderYearInput()]; + return {content}; + } + + private renderMonthPicker(): VNode { + const activeMonth = this.activeDate.getMonth(); + const monthData = this.localeData.months[this.monthStyle]; + return ( + (this.monthPickerEl = el)} + width="auto" + > + {monthData.map((month: string, index: number) => { + return ( + + {month} + + ); + })} + + ); + } + + private renderYearInput(): VNode { + const suffix = this.localeData.year?.suffix; + const localizedYear = this.formatCalendarYear(this.activeDate.getFullYear()); + return ( + + (this.yearInputEl = el)} + type="text" + value={localizedYear} + /> + {suffix && {suffix}} + + ); + } + + private renderChevron(direction: "left" | "right"): VNode { + const isDirectionRight = direction === "right"; + const isDisabled = + hasSameMonthAndYear( + isDirectionRight ? this.nextMonthDate : this.prevMonthDate, + this.activeDate, + ) || !inRange(this.activeDate, this.min, this.max); + + return ( + (isDirectionRight ? (this.nextMonthAction = el) : (this.prevMonthAction = el))} + role="button" + scale={this.scale === "l" ? "l" : "m"} + text={isDirectionRight ? this.messages.nextMonth : this.messages.prevMonth} + /> + ); + } + //-------------------------------------------------------------------------- // // Private State/Props @@ -189,31 +289,28 @@ export class DatePickerMonthHeader { @Element() el: HTMLCalciteDatePickerMonthHeaderElement; - private yearInput: HTMLInputElement; - - private parentDatePickerEl: HTMLCalciteDatePickerElement; - @State() nextMonthDate: Date; @State() prevMonthDate: Date; - @Watch("min") - @Watch("max") - @Watch("activeDate") - setNextPrevMonthDates(): void { - if (!this.activeDate) { - return; - } + private monthPickerEl: HTMLCalciteSelectElement; - this.nextMonthDate = dateFromRange(nextMonth(this.activeDate), this.min, this.max); - this.prevMonthDate = dateFromRange(prevMonth(this.activeDate), this.min, this.max); - } + private nextMonthAction: HTMLCalciteActionElement; + + private parentDatePickerEl: HTMLCalciteDatePickerElement; + + private prevMonthAction: HTMLCalciteActionElement; + + private yearSelectWidthOffset: number; + + private yearInputEl: HTMLInputElement; //-------------------------------------------------------------------------- // // Private Methods // //-------------------------------------------------------------------------- + /** * Increment year on UP/DOWN keys * @@ -279,9 +376,28 @@ export class DatePickerMonthHeader { /* * Update active month on clicks of left/right arrows */ - private handleArrowClick = (event: MouseEvent | KeyboardEvent, date: Date): void => { + private handleArrowClick = async ( + event: MouseEvent | KeyboardEvent, + date: Date, + ): Promise => { event.preventDefault(); - this.calciteInternalDatePickerSelect.emit(date); + + await this.handlePenultimateValidMonth(event); + this.calciteInternalDatePickerMonthHeaderSelectChange.emit(date); + }; + + private handleMonthChange = (event: CustomEvent): void => { + const target = event.target as HTMLCalciteOptionElement; + const { abbreviated, wide } = this.localeData.months; + const localeMonths = this.monthStyle === "wide" ? wide : abbreviated; + const monthIndex = localeMonths.indexOf(target.value); + let newDate = getDateInMonth(this.activeDate, monthIndex); + + if (!inRange(newDate, this.min, this.max)) { + newDate = dateFromRange(newDate, this.min, this.max); + } + this.calciteInternalDatePickerMonthHeaderSelectChange.emit(newDate); + this.setYearSelectMenuWidth(); }; private getInRangeDate({ @@ -323,16 +439,75 @@ export class DatePickerMonthHeader { commit?: boolean; offset?: number; }): void { - const { yearInput, activeDate } = this; + const { yearInputEl, activeDate } = this; const inRangeDate = this.getInRangeDate({ localizedYear, offset }); // if you've supplied a year and it's in range, update active date if (inRangeDate) { - this.calciteInternalDatePickerSelect.emit(inRangeDate); + this.calciteInternalDatePickerMonthHeaderSelectChange.emit(inRangeDate); } if (commit) { - yearInput.value = this.formatCalendarYear((inRangeDate || activeDate).getFullYear()); + yearInputEl.value = this.formatCalendarYear((inRangeDate || activeDate).getFullYear()); + } + } + + private setYearSelectWidthOffset(): void { + this.yearSelectWidthOffset = ICON_WIDTH_M + 3 * parseInt(this.getYearSelectPadding()); + this.setYearSelectMenuWidth(); + } + + private setYearSelectMenuWidth(): void { + const fontStyle = getComputedStyle(this.monthPickerEl).font; + const localeMonths = this.localeData.months[this.monthStyle]; + const activeLocaleMonth = localeMonths[this.activeDate.getMonth()]; + const selectedOptionWidth = getTextWidth(activeLocaleMonth, fontStyle); + this.monthPickerEl.style.width = `${selectedOptionWidth + this.yearSelectWidthOffset}px`; + } + + private isMonthInRange = (index: number): boolean => { + const newActiveDate = getDateInMonth(this.activeDate, index); + + if (!this.min && !this.max) { + return true; + } + return (!!this.max && newActiveDate < this.max) || (!!this.min && newActiveDate > this.min); + }; + + private async handlePenultimateValidMonth(event: MouseEvent | KeyboardEvent): Promise { + const target = event.target as HTMLCalciteActionElement; + const direction = target.getAttribute("data-direction"); + const isDirectionLeft = direction === "left"; + + let isTargetLastValidMonth: boolean; + + if (isDirectionLeft && this.min) { + const prevMonthDate = dateFromRange(prevMonth(this.activeDate), this.min, this.max); + isTargetLastValidMonth = hasSameMonthAndYear(prevMonthDate, this.min); + } else if (this.max) { + const nextMonthDate = dateFromRange(nextMonth(this.activeDate), this.min, this.max); + isTargetLastValidMonth = hasSameMonthAndYear(nextMonthDate, this.max); + } + + if (isTargetLastValidMonth) { + if (!this.position) { + isDirectionLeft + ? await this.nextMonthAction.setFocus() + : await this.prevMonthAction.setFocus(); + } else { + this.yearInputEl.focus(); + } + } + } + + private getYearSelectPadding(): string { + switch (this.scale) { + case "l": + return calciteSizeSm; + case "s": + return calciteSizeXxxs; + default: + return calciteSizeXxs; } } } diff --git a/packages/calcite-components/src/components/date-picker-month-header/resources.ts b/packages/calcite-components/src/components/date-picker-month-header/resources.ts index 59cc3177eb9..b12806eac87 100644 --- a/packages/calcite-components/src/components/date-picker-month-header/resources.ts +++ b/packages/calcite-components/src/components/date-picker-month-header/resources.ts @@ -1,14 +1,19 @@ export const CSS = { header: "header", - month: "month", chevron: "chevron", + chevronContainer: "chevron-container", + chevronContainerLeft: "chevron-container--left", + chevronContainerRight: "chevron-container--right", + monthYearContainer: "month-year-container", + monthPicker: "month-select", + rangeCalendar: "range-calendar", suffix: "suffix", - yearSuffix: "year--suffix", - yearWrap: "year-wrap", - textReverse: "text--reverse", + yearContainer: "year-container", }; export const ICON = { chevronLeft: "chevron-left", chevronRight: "chevron-right", } as const; + +export const ICON_WIDTH_M = 16; diff --git a/packages/calcite-components/src/components/date-picker-month/date-picker-month.scss b/packages/calcite-components/src/components/date-picker-month/date-picker-month.scss index a98c3797a0e..0421a7ebe7f 100644 --- a/packages/calcite-components/src/components/date-picker-month/date-picker-month.scss +++ b/packages/calcite-components/src/components/date-picker-month/date-picker-month.scss @@ -1,55 +1,108 @@ @include base-component(); +.calendar-container { + @apply flex w-full; +} + +:host([range][layout="vertical"]) .calendar-container { + @apply flex-col; +} + .calendar { - @apply mb-1; + @apply w-full; } -.week-headers { - @apply border-color-3 - flex - border-0 - border-t - border-solid - py-0 - px-1; +.week-header-container { + @apply flex; + block-size: #{$calcite-size-16}; + padding-inline: var(--calcite-size-sm); + padding-block: var(--calcite-size-md); } .week-header { @apply text-color-3 text-center - font-bold; + font-bold + flex + text-n2h + justify-center + items-center; inline-size: calc(100% / 7); } -.day { +.day-container { @apply flex min-w-0 - justify-center; - inline-size: 100%; + justify-center + w-full; calcite-date-picker-day { @apply w-full; } } -:host([scale="s"]) .week-header { - @apply text-n2h px-0 pt-2 pb-3; +.week-days { + @apply grid; + grid-template-columns: repeat(7, 1fr); + grid-auto-rows: 1fr; + padding-inline: var(--calcite-size-sm); + padding-block-end: var(--calcite-size-sm); } -:host([scale="m"]) .week-header { - @apply text-n2h px-0 pt-3 pb-4; +.month-header { + @apply flex w-full justify-between; } -:host([scale="l"]) .week-header { - @apply text-n1h px-0 pt-4 pb-5; +.month { + @apply flex w-full justify-between flex-col; } -.week-days { - display: grid; - grid-template-columns: repeat(7, 1fr); - grid-auto-rows: 1fr; - padding-block: 0; - padding-inline: 6px; - &:focus { - @apply outline-none; +.day { + font-size: var(--calcite-font-size); +} + +:host([scale="s"]) { + .week-days { + padding-inline: var(--calcite-size-xs); + padding-block-end: var(--calcite-size-xs); + } + .week-header-container { + padding-inline: var(--calcite-size-xs); + padding-block: var(--calcite-size-sm); } + .day { + font-size: var(--calcite-font-size-sm); + } +} + +:host([scale="l"]) { + .week-header { + @apply text-n1h; + } + .week-days { + padding-inline: var(--calcite-size-md); + padding-block-end: var(--calcite-size-md); + } + .week-header-container { + padding-inline: var(--calcite-size-md); + padding-block: var(--calcite-size-md-plus); + } + .day { + font-size: var(--calcite-font-size-md); + } +} + +.calendar--start { + @apply border-solid border-color-1 border-0; +} + +:host([range][layout="horizontal"]) .calendar--start { + border-inline-end-width: var(--calcite-border-width-sm); +} + +:host([range][layout="vertical"]) .calendar--start { + border-block-end-width: var(--calcite-border-width-sm); +} + +.noncurrent { + @apply pointer-events-none opacity-0; } diff --git a/packages/calcite-components/src/components/date-picker-month/date-picker-month.tsx b/packages/calcite-components/src/components/date-picker-month/date-picker-month.tsx index 198d242f8be..bb72a00133b 100644 --- a/packages/calcite-components/src/components/date-picker-month/date-picker-month.tsx +++ b/packages/calcite-components/src/components/date-picker-month/date-picker-month.tsx @@ -8,10 +8,23 @@ import { Listen, Prop, VNode, + Watch, + State, } from "@stencil/core"; -import { dateFromRange, HoverRange, inRange, sameDate } from "../../utils/date"; +import { + dateFromRange, + getFirstValidDateInMonth, + hasSameMonthAndYear, + HoverRange, + inRange, + nextMonth, + sameDate, +} from "../../utils/date"; import { DateLocaleData } from "../date-picker/utils"; import { Scale } from "../interfaces"; +import { DatePickerMessages } from "../date-picker/assets/date-picker/t9n"; +import { HeadingLevel } from "../functional/Heading"; +import { CSS } from "./resources"; const DAYS_PER_WEEK = 7; const DAYS_MAXIMUM_INDEX = 6; @@ -19,6 +32,7 @@ const DAYS_MAXIMUM_INDEX = 6; interface Day { active: boolean; currentMonth?: boolean; + currentDay?: boolean; date: Date; day: number; dayInWeek?: number; @@ -37,6 +51,18 @@ export class DatePickerMonth { // //-------------------------------------------------------------------------- + /** The currently active Date.*/ + @Prop() activeDate: Date = new Date(); + + @Watch("activeDate") + updateFocusedDateWithActive(newActiveDate: Date): void { + if (!this.selectedDate) { + this.focusedDate = inRange(newActiveDate, this.min, this.max) + ? newActiveDate + : dateFromRange(newActiveDate, this.min, this.max); + } + } + /** * The DateTimeFormat used to provide screen reader labels. * @@ -44,26 +70,21 @@ export class DatePickerMonth { */ @Prop() dateTimeFormat: Intl.DateTimeFormat; - /** Already selected date.*/ - @Prop() selectedDate: Date; - - /** The currently active Date.*/ - @Prop() activeDate: Date = new Date(); - - /** Start date currently active. */ - @Prop() startDate?: Date; - /** End date currently active. */ @Prop() endDate?: Date; - /** Specifies the earliest allowed date (`"yyyy-mm-dd"`). */ - @Prop() min: Date; + /** Specifies the number at which section headings should start. */ + @Prop({ reflect: true }) headingLevel: HeadingLevel; - /** Specifies the latest allowed date (`"yyyy-mm-dd"`). */ - @Prop() max: Date; + /** The range of dates currently being hovered. */ + @Prop() hoverRange: HoverRange; - /** Specifies the size of the component. */ - @Prop({ reflect: true }) scale: Scale; + /** + * Specifies the layout of the component. + * + * @internal + */ + @Prop({ reflect: true }) layout: "horizontal" | "vertical"; /** * CLDR locale data for current locale. @@ -72,8 +93,42 @@ export class DatePickerMonth { */ @Prop() localeData: DateLocaleData; - /** The range of dates currently being hovered. */ - @Prop() hoverRange: HoverRange; + /** Specifies the latest allowed date (`"yyyy-mm-dd"`). */ + @Prop() max: Date; + + /** + * Made into a prop for testing purposes only + * + * @internal + */ + @Prop() messages: DatePickerMessages; + + /** Specifies the earliest allowed date (`"yyyy-mm-dd"`). */ + @Prop() min: Date; + + /** + * Specifies the monthStyle used by the component. + */ + @Prop() monthStyle: "abbreviated" | "wide"; + + /** + * When `true`, activates the component's range mode which renders two calendars for selecting ranges of dates. + */ + @Prop({ reflect: true }) range: boolean = false; + + /** Specifies the size of the component. */ + @Prop({ reflect: true }) scale: Scale; + + /** Already selected date.*/ + @Prop() selectedDate: Date; + + @Watch("selectedDate") + updateFocusedDate(newActiveDate: Date): void { + this.focusedDate = newActiveDate; + } + + /** Start date currently active. */ + @Prop() startDate?: Date; //-------------------------------------------------------------------------- // @@ -86,26 +141,44 @@ export class DatePickerMonth { * * @internal */ - @Event({ cancelable: false }) calciteInternalDatePickerSelect: EventEmitter; + @Event({ cancelable: false }) calciteInternalDatePickerDaySelect: EventEmitter; /** * Fires when user hovers the date. * * @internal */ - @Event({ cancelable: false }) calciteInternalDatePickerHover: EventEmitter; + @Event({ cancelable: false }) calciteInternalDatePickerDayHover: EventEmitter; /** * Active date for the user keyboard access. * * @internal */ - @Event({ cancelable: false }) calciteInternalDatePickerActiveDateChange: EventEmitter; + @Event({ cancelable: false }) calciteInternalDatePickerMonthActiveDateChange: EventEmitter; + + /** + * @internal + */ + @Event({ cancelable: false }) calciteInternalDatePickerMonthMouseOut: EventEmitter; /** + * Emits when user updates month or year using `calcite-date-picker-month-header` component. + * * @internal */ - @Event({ cancelable: false }) calciteInternalDatePickerMouseOut: EventEmitter; + @Event({ cancelable: false }) calciteInternalDatePickerMonthChange: EventEmitter<{ + date: Date; + position: string; + }>; + + //-------------------------------------------------------------------------- + // + // Private State/Props + // + //-------------------------------------------------------------------------- + + @State() focusedDate: Date; //-------------------------------------------------------------------------- // @@ -119,43 +192,44 @@ export class DatePickerMonth { } const isRTL = this.el.dir === "rtl"; + const dateValue = (event.target as HTMLCalciteDatePickerDayElement).value; switch (event.key) { case "ArrowUp": event.preventDefault(); - this.addDays(-7); + this.addDays(-7, dateValue); break; case "ArrowRight": event.preventDefault(); - this.addDays(isRTL ? -1 : 1); + this.addDays(isRTL ? -1 : 1, dateValue); break; case "ArrowDown": event.preventDefault(); - this.addDays(7); + this.addDays(7, dateValue); break; case "ArrowLeft": event.preventDefault(); - this.addDays(isRTL ? 1 : -1); + this.addDays(isRTL ? 1 : -1, dateValue); break; case "PageUp": event.preventDefault(); - this.addMonths(-1); + this.addMonths(-1, dateValue); break; case "PageDown": event.preventDefault(); - this.addMonths(1); + this.addMonths(1, dateValue); break; case "Home": event.preventDefault(); this.activeDate.setDate(1); - this.addDays(); + this.addDays(0, dateValue); break; case "End": event.preventDefault(); this.activeDate.setDate( new Date(this.activeDate.getFullYear(), this.activeDate.getMonth() + 1, 0).getDate(), ); - this.addDays(); + this.addDays(0, dateValue); break; case "Enter": case " ": @@ -176,7 +250,7 @@ export class DatePickerMonth { @Listen("pointerout") pointerOutHandler(): void { - this.calciteInternalDatePickerMouseOut.emit(); + this.calciteInternalDatePickerMonthMouseOut.emit(); } //-------------------------------------------------------------------------- @@ -184,6 +258,11 @@ export class DatePickerMonth { // Lifecycle // //-------------------------------------------------------------------------- + + componentWillLoad(): void { + this.focusedDate = this.selectedDate || this.activeDate; + } + render(): VNode { const month = this.activeDate.getMonth(); const year = this.activeDate.getFullYear(); @@ -195,54 +274,24 @@ export class DatePickerMonth { const curMonDays = this.getCurrentMonthDays(month, year); const prevMonDays = this.getPreviousMonthDays(month, year, startOfWeek); const nextMonDays = this.getNextMonthDays(month, year, startOfWeek); - let dayInWeek = 0; - const getDayInWeek = () => dayInWeek++ % 7; - - const days: Day[] = [ - ...prevMonDays.map((day) => { - return { - active: false, - day, - dayInWeek: getDayInWeek(), - date: new Date(year, month - 1, day), - }; - }), - ...curMonDays.map((day) => { - const date = new Date(year, month, day); - const active = sameDate(date, this.activeDate); - return { - active, - currentMonth: true, - day, - dayInWeek: getDayInWeek(), - date, - ref: true, - }; - }), - ...nextMonDays.map((day) => { - return { - active: false, - day, - dayInWeek: getDayInWeek(), - date: new Date(year, month + 1, day), - }; - }), - ]; + const nextMonth = month + 1; + const endCalendarPrevMonDays = this.getPreviousMonthDays(nextMonth, year, startOfWeek); + const endCalendarCurrMonDays = this.getCurrentMonthDays(nextMonth, year); + const endCalendarNextMonDays = this.getNextMonthDays(nextMonth, year, startOfWeek); + const days = this.getDays(prevMonDays, curMonDays, nextMonDays); + + const nextMonthDays = this.getDays( + endCalendarPrevMonDays, + endCalendarCurrMonDays, + endCalendarNextMonDays, + "end", + ); return ( - -
-
- {adjustedWeekDays.map((weekday) => ( - - {weekday} - - ))} -
- -
- {days.map((day, index) => this.renderDateDay(day, index))} -
+ +
+ {this.renderCalendar(adjustedWeekDays, days)} + {this.range && this.renderCalendar(adjustedWeekDays, nextMonthDays, true)}
); @@ -267,28 +316,35 @@ export class DatePickerMonth { * Add n months to the current month * * @param step + * @param targetDate */ - private addMonths(step: number) { - const nextDate = new Date(this.activeDate); - nextDate.setMonth(this.activeDate.getMonth() + step); - this.calciteInternalDatePickerActiveDateChange.emit( + private addMonths(step: number, targetDate: Date): void { + const nextDate = new Date(targetDate); + nextDate.setMonth(targetDate.getMonth() + step); + this.calciteInternalDatePickerMonthActiveDateChange.emit( dateFromRange(nextDate, this.min, this.max), ); + this.focusedDate = dateFromRange(nextDate, this.min, this.max); this.activeFocus = true; + this.calciteInternalDatePickerDayHover.emit(nextDate); } /** * Add n days to the current date * * @param step + * @param targetDate */ - private addDays(step = 0) { - const nextDate = new Date(this.activeDate); - nextDate.setDate(this.activeDate.getDate() + step); - this.calciteInternalDatePickerActiveDateChange.emit( + private addDays(step = 0, targetDate: Date): void { + const nextDate = new Date(targetDate); + nextDate.setDate(targetDate.getDate() + step); + this.calciteInternalDatePickerMonthActiveDateChange.emit( dateFromRange(nextDate, this.min, this.max), ); + + this.focusedDate = dateFromRange(nextDate, this.min, this.max); this.activeFocus = true; + this.calciteInternalDatePickerDayHover.emit(nextDate); } /** @@ -407,16 +463,18 @@ export class DatePickerMonth { dayHover = (event: CustomEvent): void => { const target = event.target as HTMLCalciteDatePickerDayElement; if (target.disabled) { - this.calciteInternalDatePickerMouseOut.emit(); + this.calciteInternalDatePickerMonthMouseOut.emit(); } else { - this.calciteInternalDatePickerHover.emit(target.value); + this.calciteInternalDatePickerDayHover.emit(target.value); } event.stopPropagation(); }; daySelect = (event: CustomEvent): void => { const target = event.target as HTMLCalciteDatePickerDayElement; - this.calciteInternalDatePickerSelect.emit(target.value); + this.activeFocus = false; + this.calciteInternalDatePickerDaySelect.emit(target.value); + event.stopPropagation(); }; /** @@ -435,34 +493,35 @@ export class DatePickerMonth { * @param active.dayInWeek * @param active.ref * @param key + * @param active.currentDay */ - private renderDateDay({ active, currentMonth, date, day, dayInWeek, ref }: Day, key: number) { - const isFocusedOnStart = this.isFocusedOnStart(); - const isHoverInRange = - this.isHoverInRange() || - (!this.endDate && this.hoverRange && sameDate(this.hoverRange?.end, this.startDate)); + private renderDateDay( + { active, currentMonth, currentDay, date, day, dayInWeek, ref }: Day, + key: number, + ): VNode { + const isDateInRange = inRange(date, this.min, this.max); return ( -
+
{ // when moving via keyboard, focus must be updated on active date if (ref && active && this.activeFocus) { @@ -478,39 +537,187 @@ export class DatePickerMonth { ); } + private renderCalendar(weekDays: string[], days: Day[], isEndCalendar = false): VNode { + return ( +
+ + {this.renderMonthCalendar(weekDays, days, isEndCalendar)} +
+ ); + } + private isFocusedOnStart(): boolean { return this.hoverRange?.focused === "start"; } private isHoverInRange(): boolean { - if (!this.hoverRange) { + if (!this.hoverRange || !this.startDate) { return false; } const { start, end } = this.hoverRange; - return !!( - (!this.isFocusedOnStart() && this.startDate && (!this.endDate || end < this.endDate)) || - (this.isFocusedOnStart() && this.startDate && start > this.startDate) - ); + const isStartFocused = this.isFocusedOnStart(); + const isEndAfterStart = this.startDate && end > this.startDate; + const isEndBeforeEnd = this.endDate && end < this.endDate; + const isStartAfterStart = this.startDate && start > this.startDate; + const isStartBeforeEnd = this.endDate && start < this.endDate; + + const isEndDateAfterStartAndBeforeEnd = + !isStartFocused && this.startDate && isEndAfterStart && (!this.endDate || isEndBeforeEnd); + const isStartDateBeforeEndAndAfterStart = + isStartFocused && this.startDate && isStartAfterStart && isStartBeforeEnd; + + return isEndDateAfterStartAndBeforeEnd || isStartDateBeforeEndAndAfterStart; } - private isRangeHover(date): boolean { + private isRangeHover(date: Date): boolean { if (!this.hoverRange) { return false; } const { start, end } = this.hoverRange; - const isStart = this.isFocusedOnStart(); + const isStartFocused = this.isFocusedOnStart(); const insideRange = this.isHoverInRange(); - const cond1 = - insideRange && - ((!isStart && date > this.startDate && (date < end || sameDate(date, end))) || - (isStart && date < this.endDate && (date > start || sameDate(date, start)))); - const cond2 = - !insideRange && - ((!isStart && date >= this.endDate && (date < end || sameDate(date, end))) || - (isStart && - ((this.startDate && date < this.startDate) || - (this.endDate && sameDate(date, this.startDate))) && - ((start && date > start) || sameDate(date, start)))); - return cond1 || cond2; + + const isDateBeforeStartDateAndAfterStart = date > start && date < this.startDate; + const isDateAfterEndDateAndBeforeEnd = date < end && date > this.endDate; + const isDateBeforeEndDateAndAfterEnd = date > end && date < this.endDate; + const isDateAfterStartDateAndBeforeStart = date < start && date > this.startDate; + const isDateAfterStartDateAndBeforeEnd = date < end && date > this.startDate; + const isDateBeforeEndDateAndAfterStart = date > start && date < this.endDate; + const hasBothStartAndEndDate = this.startDate && this.endDate; + + if (insideRange) { + if (hasBothStartAndEndDate) { + return isStartFocused + ? date < this.endDate && + (isDateAfterStartDateAndBeforeStart || isDateBeforeStartDateAndAfterStart) + : isDateBeforeEndDateAndAfterEnd || isDateAfterEndDateAndBeforeEnd; + } else if (this.startDate && !this.endDate) { + return isStartFocused + ? isDateBeforeStartDateAndAfterStart + : isDateAfterStartDateAndBeforeEnd; + } else if (!this.startDate && this.endDate) { + return isStartFocused ? isDateBeforeEndDateAndAfterStart : isDateAfterEndDateAndBeforeEnd; + } + } else { + if (hasBothStartAndEndDate) { + return isStartFocused ? isDateBeforeStartDateAndAfterStart : isDateAfterEndDateAndBeforeEnd; + } + } + } + + private getDays( + prevMonthDays: number[], + currMonthDays: number[], + nextMonthDays: number[], + position: "start" | "end" = "start", + ): Day[] { + let month = this.activeDate.getMonth(); + const nextMonth = month + 1; + month = position === "end" ? nextMonth : month; + let dayInWeek = 0; + const getDayInWeek = () => dayInWeek++ % 7; + const year = this.activeDate.getFullYear(); + + const days: Day[] = [ + ...prevMonthDays.map((day) => { + return { + active: false, + day, + dayInWeek: getDayInWeek(), + date: new Date(year, month - 1, day), + }; + }), + ...currMonthDays.map((day) => { + const date = new Date(year, month, day); + const isCurrentDay = sameDate(date, new Date()); + const active = + this.focusedDate && + this.focusedDate !== this.startDate && + this.focusedDate !== this.endDate + ? sameDate(date, this.focusedDate) + : sameDate(date, this.startDate) || sameDate(date, this.endDate); + + return { + active, + currentMonth: true, + currentDay: isCurrentDay, + day, + dayInWeek: getDayInWeek(), + date, + ref: true, + }; + }), + ...nextMonthDays.map((day) => { + return { + active: false, + day, + dayInWeek: getDayInWeek(), + date: new Date(year, nextMonth, day), + }; + }), + ]; + + return days; + } + + private renderMonthCalendar(weekDays: string[], days: Day[], isEndCalendar = false): VNode { + const endCalendarStartIndex = 50; + return ( +
+
+ {weekDays.map((weekday) => ( + + {weekday} + + ))} +
+ +
+ {days.map((day, index) => + this.renderDateDay(day, isEndCalendar ? endCalendarStartIndex + index : index), + )} +
+
+ ); + } + + private monthHeaderSelectChange = (event: CustomEvent): void => { + const date = new Date(event.detail); + const target = event.target as HTMLCalciteDatePickerMonthHeaderElement; + this.updateFocusableDate(date); + event.stopPropagation(); + this.calciteInternalDatePickerMonthChange.emit({ date, position: target.position }); + }; + + private updateFocusableDate(date: Date): void { + if (!this.selectedDate || !this.range) { + this.focusedDate = this.getFirstValidDateOfMonth(date); + } else if (this.selectedDate && this.range) { + if (!hasSameMonthAndYear(this.startDate, date) || !hasSameMonthAndYear(this.endDate, date)) { + this.focusedDate = this.getFirstValidDateOfMonth(date); + } + } + } + + private getFirstValidDateOfMonth(date: Date): Date { + return date.getDate() === 1 ? date : getFirstValidDateInMonth(date, this.min, this.max); } } diff --git a/packages/calcite-components/src/components/date-picker-month/resources.ts b/packages/calcite-components/src/components/date-picker-month/resources.ts new file mode 100644 index 00000000000..6448cfe50cb --- /dev/null +++ b/packages/calcite-components/src/components/date-picker-month/resources.ts @@ -0,0 +1,14 @@ +export const CSS = { + calendar: "calendar", + calendarContainer: "calendar-container", + calendarStart: "calendar--start", + currentDay: "current-day", + dayContainer: "day-container", + insideRangeHover: "inside-range--hover", + month: "month", + noncurrent: "noncurrent", + outsideRangeHover: "outside-range--hover", + weekDays: "week-days", + weekHeader: "week-header", + weekHeaderContainer: "week-header-container", +}; diff --git a/packages/calcite-components/src/components/date-picker/date-picker.e2e.ts b/packages/calcite-components/src/components/date-picker/date-picker.e2e.ts index ef20879141f..589a3ba0c78 100644 --- a/packages/calcite-components/src/components/date-picker/date-picker.e2e.ts +++ b/packages/calcite-components/src/components/date-picker/date-picker.e2e.ts @@ -1,8 +1,11 @@ -import { E2EPage, newE2EPage } from "@stencil/core/testing"; +import { E2EElement, E2EPage, newE2EPage } from "@stencil/core/testing"; import { html } from "../../../support/formatting"; import { defaults, focusable, hidden, renders, t9n } from "../../tests/commonTests"; import { skipAnimations } from "../../tests/utils"; import { formatTimePart } from "../../utils/time"; +import { Position } from "../interfaces"; +import { CSS as MONTH_CSS } from "../date-picker-month/resources"; +import { CSS as MONTH_HEADER_CSS } from "../date-picker-month-header/resources"; describe("calcite-date-picker", () => { describe("renders", () => { @@ -24,257 +27,166 @@ describe("calcite-date-picker", () => { describe("focusable", () => { focusable("calcite-date-picker", { - shadowFocusTargetSelector: "calcite-date-picker-month-header", + shadowFocusTargetSelector: "calcite-date-picker-month", }); }); const animationDurationInMs = 200; - it("fires a calciteDatePickerChange event when changing year in header", async () => { - const page = await newE2EPage(); - await page.setContent(""); - const date = await page.find("calcite-date-picker"); - const changedEvent = await page.spyOnEvent("calciteDatePickerChange"); - - await page.waitForTimeout(animationDurationInMs); - // can't find this input as it's deeply nested in shadow dom, so just tab to it - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("ArrowUp"); - await page.waitForChanges(); - expect(changedEvent).toHaveReceivedEventTimes(0); - const value = await date.getProperty("value"); - expect(value).toEqual("2000-11-27"); - await page.keyboard.press("ArrowDown"); - const value2 = await date.getProperty("value"); - expect(value2).toEqual("2000-11-27"); - expect(changedEvent).toHaveReceivedEventTimes(0); - }); - - it("updates the calendar immediately as a new year is typed but doesn't change the year", async () => { - const page = await newE2EPage(); - await page.setContent(``); - const datePicker = await page.find("calcite-date-picker"); - await page.waitForTimeout(animationDurationInMs); - - async function getActiveMonthDate(): Promise { - return page.$eval("calcite-date-picker", (datePicker: HTMLCalciteDatePickerElement) => - datePicker.shadowRoot.querySelector("calcite-date-picker-month").activeDate.toISOString(), - ); - } - - async function getActiveMonthHeaderInputValue(): Promise { - return page.$eval( - "calcite-date-picker", - (datePicker: HTMLCalciteDatePickerElement) => - datePicker.shadowRoot - .querySelector("calcite-date-picker-month-header") - .shadowRoot.querySelector(".year").value, - ); - } - - const activeDateBefore = await getActiveMonthDate(); - - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.down("Meta"); - await page.keyboard.press("a"); - expect(await getActiveMonthHeaderInputValue()).toBe("2015"); - await page.keyboard.press("Backspace"); - await page.keyboard.up("Meta"); - await page.keyboard.type("2016"); - expect(await getActiveMonthHeaderInputValue()).toBe("2016"); - await page.waitForChanges(); + describe("calciteDatePickerChange & calciteDatePickerRangeChange events", () => { + it("fires a calciteDatePickerChange event when changing year in header", async () => { + const page = await newE2EPage(); + await page.setContent(""); + const date = await page.find("calcite-date-picker"); + const changedEvent = await page.spyOnEvent("calciteDatePickerChange"); - const activeDateAfter = await getActiveMonthDate(); + await page.waitForTimeout(animationDurationInMs); + // can't find this input as it's deeply nested in shadow dom, so just tab to it + await page.keyboard.press("Tab"); + await page.keyboard.press("Tab"); + await page.keyboard.press("Tab"); + await page.keyboard.press("Tab"); + await page.keyboard.press("Tab"); + await page.keyboard.press("ArrowUp"); + await page.waitForChanges(); + expect(changedEvent).toHaveReceivedEventTimes(0); + const value = await date.getProperty("value"); + expect(value).toEqual("2000-11-27"); + await page.keyboard.press("ArrowDown"); + const value2 = await date.getProperty("value"); + expect(value2).toEqual("2000-11-27"); + expect(changedEvent).toHaveReceivedEventTimes(0); + }); - expect(activeDateBefore).not.toEqual(activeDateAfter); - expect(await datePicker.getProperty("value")).toBe("2015-02-28"); - }); + it("fires a calciteDatePickerChange event when day is selected", async () => { + const page = await newE2EPage(); + await page.setContent(""); + const changedEvent = await page.spyOnEvent("calciteDatePickerChange"); - it("fires a calciteDatePickerChange event when day is selected", async () => { - const page = await newE2EPage(); - await page.setContent(""); - const changedEvent = await page.spyOnEvent("calciteDatePickerChange"); + await page.waitForTimeout(animationDurationInMs); - await page.waitForTimeout(animationDurationInMs); + await selectFirstAvailableDay(page); + expect(changedEvent).toHaveReceivedEventTimes(1); - await selectFirstAvailableDay(page, "mouse"); - expect(changedEvent).toHaveReceivedEventTimes(1); + await selectFirstAvailableDay(page); + expect(changedEvent).toHaveReceivedEventTimes(2); + }); - await selectFirstAvailableDay(page, "keyboard"); - expect(changedEvent).toHaveReceivedEventTimes(2); - }); + it("emits calciteDatePickerRangeChange event and updates value property when start and end dates are selected from start calendar", async () => { + const page = await newE2EPage(); + await page.setContent(""); + const datePicker = await page.find("calcite-date-picker"); + const eventSpy = await page.spyOnEvent("calciteDatePickerRangeChange"); - it("Emits change event and updates value property when start and end dates are selected", async () => { - const page = await newE2EPage(); - await page.setContent(""); - const datePicker = await page.find("calcite-date-picker"); - const eventSpy = await page.spyOnEvent("calciteDatePickerRangeChange"); + await page.waitForTimeout(animationDurationInMs); - await page.waitForTimeout(animationDurationInMs); + const now = new Date(); + const currentYear = now.getUTCFullYear(); + const currentMonth = now.getUTCMonth() + 1; + const startDate = `${currentYear}-${formatTimePart(currentMonth)}-01`; + const endDate = `${currentYear}-${formatTimePart(currentMonth)}-15`; - const now = new Date(); - const currentYear = now.getUTCFullYear(); - const currentMonth = now.getUTCMonth() + 1; - const startDate = `${currentYear}-${formatTimePart(currentMonth)}-01`; - const endDate = `${currentYear}-${formatTimePart(currentMonth)}-15`; + await selectDayInMonthById(startDate.replaceAll("-", ""), page); + await page.waitForChanges(); - await selectDay(startDate.replaceAll("-", ""), page, "mouse"); - await page.waitForChanges(); + expect(await datePicker.getProperty("value")).toEqual([startDate, ""]); - expect(await datePicker.getProperty("value")).toEqual([startDate, ""]); + await selectDayInMonthById(endDate.replaceAll("-", ""), page); + await page.waitForChanges(); - await selectDay(endDate.replaceAll("-", ""), page, "mouse"); - await page.waitForChanges(); + expect(await datePicker.getProperty("value")).toEqual([startDate, endDate]); + expect(eventSpy).toHaveReceivedEventTimes(2); + }); - expect(await datePicker.getProperty("value")).toEqual([startDate, endDate]); - expect(eventSpy).toHaveReceivedEventTimes(2); - }); + it("Emits calciteDatePickerRangeChange event and updates value property when start and end dates are selected from end calendar", async () => { + const page = await newE2EPage(); + await page.setContent(""); + const datePicker = await page.find("calcite-date-picker"); + const eventSpy = await page.spyOnEvent("calciteDatePickerRangeChange"); - it("doesn't fire calciteDatePickerChange when the selected day is selected", async () => { - const page = await newE2EPage(); - await page.setContent(""); - const changedEvent = await page.spyOnEvent("calciteDatePickerChange"); + await page.waitForTimeout(animationDurationInMs); - await skipAnimations(page); + const now = new Date(); + const currentYear = now.getUTCFullYear(); + const currentMonth = now.getUTCMonth() + 2; + const startDate = `${currentYear}-${formatTimePart(currentMonth)}-01`; + const endDate = `${currentYear}-${formatTimePart(currentMonth)}-15`; - await selectSelectedDay(page, "mouse"); - expect(changedEvent).toHaveReceivedEventTimes(0); - await selectSelectedDay(page, "mouse"); - expect(changedEvent).toHaveReceivedEventTimes(0); - await selectSelectedDay(page, "mouse"); - expect(changedEvent).toHaveReceivedEventTimes(0); - - await selectSelectedDay(page, "keyboard"); - expect(changedEvent).toHaveReceivedEventTimes(0); - await selectSelectedDay(page, "keyboard"); - expect(changedEvent).toHaveReceivedEventTimes(0); - await selectSelectedDay(page, "keyboard"); - expect(changedEvent).toHaveReceivedEventTimes(0); - }); + await selectDayInMonthById(startDate.replaceAll("-", ""), page); + await page.waitForChanges(); - async function selectDay(id: string, page: E2EPage, method: "mouse" | "keyboard"): Promise { - await page.$eval( - "calcite-date-picker", - (datePicker: HTMLCalciteDatePickerElement, id: string, method: "mouse" | "keyboard") => { - const day = datePicker.shadowRoot - .querySelector("calcite-date-picker-month") - .shadowRoot.getElementById(id); - - if (method === "mouse") { - day.click(); - } else { - day.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" })); - } - }, - id, - method, - ); - await page.waitForChanges(); - } + expect(await datePicker.getProperty("value")).toEqual([startDate, ""]); - async function selectFirstAvailableDay(page: E2EPage, method: "mouse" | "keyboard"): Promise { - await page.$eval( - "calcite-date-picker", - (datePicker: HTMLCalciteDatePickerElement, method: "mouse" | "keyboard") => { - const day = datePicker.shadowRoot - .querySelector("calcite-date-picker-month") - .shadowRoot.querySelector("calcite-date-picker-day:not([selected])"); - - if (method === "mouse") { - day.click(); - } else { - day.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" })); - } - }, - method, - ); - await page.waitForChanges(); - } + await selectDayInMonthById(endDate.replaceAll("-", ""), page); + await page.waitForChanges(); - async function selectSelectedDay(page: E2EPage, method: "mouse" | "keyboard"): Promise { - await page.$eval( - "calcite-date-picker", - (datePicker: HTMLCalciteDatePickerElement, method: "mouse" | "keyboard") => { - const day = datePicker.shadowRoot - .querySelector("calcite-date-picker-month") - .shadowRoot.querySelector("calcite-date-picker-day[selected]"); - - if (method === "mouse") { - day.click(); - } else { - day.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" })); - } - }, - method, - ); - await page.waitForChanges(); - } + expect(await datePicker.getProperty("value")).toEqual([startDate, endDate]); + expect(eventSpy).toHaveReceivedEventTimes(2); + }); - it("doesn't fire calciteDatePickerChange on outside changes to value", async () => { - const page = await newE2EPage(); - await page.setContent(""); - const date = await page.find("calcite-date-picker"); - const changedEvent = await page.spyOnEvent("calciteDatePickerChange"); - date.setProperty("value", "2001-10-28"); - await page.waitForChanges(); - expect(changedEvent).toHaveReceivedEventTimes(0); - }); + it("doesn't fire calciteDatePickerChange when the selected day is selected", async () => { + const page = await newE2EPage(); + await page.setContent(""); + const changedEvent = await page.spyOnEvent("calciteDatePickerChange"); + + await skipAnimations(page); + + await selectSelectedDay(page); + expect(changedEvent).toHaveReceivedEventTimes(0); + await selectSelectedDay(page); + expect(changedEvent).toHaveReceivedEventTimes(0); + await selectSelectedDay(page); + expect(changedEvent).toHaveReceivedEventTimes(0); + + await selectSelectedDay(page); + expect(changedEvent).toHaveReceivedEventTimes(0); + await selectSelectedDay(page); + expect(changedEvent).toHaveReceivedEventTimes(0); + await selectSelectedDay(page); + expect(changedEvent).toHaveReceivedEventTimes(0); + }); - it.skip("correctly changes date on next/prev", async () => { - const page = await newE2EPage(); - await page.setContent(""); - const getMonth = () => { - return document - .querySelector("calcite-date-picker") - .shadowRoot.querySelector("calcite-date-picker-month-header") - .shadowRoot.querySelector(".month").textContent; - }; - expect(await page.evaluate(getMonth)).toEqualText("November"); - // tab to prev arrow - await page.keyboard.press("Tab"); - await page.keyboard.press("Enter"); - await page.waitForChanges(); - expect(await page.evaluate(getMonth)).toEqualText("October"); - // tab to next arrow - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Enter"); - await page.waitForChanges(); - expect(await page.evaluate(getMonth)).toEqualText("November"); - }); + it("doesn't fire calciteDatePickerChange on outside changes to value", async () => { + const page = await newE2EPage(); + await page.setContent(""); + const date = await page.find("calcite-date-picker"); + const changedEvent = await page.spyOnEvent("calciteDatePickerChange"); + date.setProperty("value", "2001-10-28"); + await page.waitForChanges(); + expect(changedEvent).toHaveReceivedEventTimes(0); + }); - it("fires calciteDatePickerRangeChange event on user change", async () => { - const page = await newE2EPage(); - await page.setContent(``); - await page.waitForChanges(); - const date = await page.find("calcite-date-picker"); - date.setProperty("value", ["2020-09-08", "2020-09-23"]); + it("fires calciteDatePickerRangeChange event on user change", async () => { + const page = await newE2EPage(); + await page.setContent(``); + await page.waitForChanges(); + const date = await page.find("calcite-date-picker"); + date.setProperty("value", ["2020-09-08", "2020-09-23"]); - // have to wait for transition - const changedEvent = await page.spyOnEvent("calciteDatePickerRangeChange"); - await new Promise((res) => global.setTimeout(() => res(true), 200)); - expect(changedEvent).toHaveReceivedEventTimes(0); + // have to wait for transition + const changedEvent = await page.spyOnEvent("calciteDatePickerRangeChange"); + await new Promise((res) => global.setTimeout(() => res(true), 200)); + expect(changedEvent).toHaveReceivedEventTimes(0); - await page.waitForChanges(); + await page.waitForChanges(); - expect(await date.getProperty("value")).toEqual(["2020-09-08", "2020-09-23"]); + expect(await date.getProperty("value")).toEqual(["2020-09-08", "2020-09-23"]); - await page.keyboard.press("Tab"); - await page.waitForChanges(); - await page.keyboard.press("Tab"); - await page.waitForChanges(); - await page.keyboard.press("Tab"); - await page.waitForChanges(); - await page.keyboard.press("Tab"); - await page.waitForChanges(); - await page.keyboard.press("ArrowRight"); - await page.waitForChanges(); - await page.keyboard.press("Space"); - await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("ArrowRight"); + await page.waitForChanges(); + await page.keyboard.press("Space"); + await page.waitForChanges(); - expect(changedEvent).toHaveReceivedEventTimes(1); + expect(changedEvent).toHaveReceivedEventTimes(1); + }); }); describe("when the lang is set to Slovak calendar", () => { @@ -295,60 +207,48 @@ describe("calcite-date-picker", () => { }); }); - it("updates internally when min attribute is updated after initialization", async () => { - const page = await newE2EPage(); - await page.emulateTimezone("America/Los_Angeles"); - await page.setContent( - html``, - ); - - const element = await page.find("calcite-date-picker"); - element.setProperty("min", "2021-11-15"); - element.setProperty("max", "2023-11-15"); - await page.waitForChanges(); - const minDateString = "Mon Nov 15 2021 00:00:00 GMT-0800 (Pacific Standard Time)"; - const minDateAsTime = await page.$eval("calcite-date-picker", (picker: HTMLCalciteDatePickerElement) => - picker.minAsDate.getTime(), - ); - expect(minDateAsTime).toEqual(new Date(minDateString).getTime()); - }); - - it("unsetting min/max updates internally", async () => { - const page = await newE2EPage(); - await page.emulateTimezone("America/Los_Angeles"); - await page.setContent( - html``, - ); + describe("min & max", () => { + it("updates minAsDate when min attribute is updated after initialization", async () => { + const page = await newE2EPage(); + await page.emulateTimezone("America/Los_Angeles"); + await page.setContent( + html``, + ); - const element = await page.find("calcite-date-picker"); + const element = await page.find("calcite-date-picker"); + element.setProperty("min", "2021-11-15"); + element.setProperty("max", "2023-11-15"); + await page.waitForChanges(); + const minDateString = "Mon Nov 15 2021 00:00:00 GMT-0800 (Pacific Standard Time)"; + const minDateAsTime = await page.$eval("calcite-date-picker", (picker: HTMLCalciteDatePickerElement) => + picker.minAsDate.getTime(), + ); + expect(minDateAsTime).toEqual(new Date(minDateString).getTime()); + }); - element.setProperty("min", null); - element.setProperty("max", null); - await page.waitForChanges(); + it("unsetting min/max updates minAsDate & maxAsDate", async () => { + const page = await newE2EPage(); + await page.emulateTimezone("America/Los_Angeles"); + await page.setContent( + html``, + ); - expect(await element.getProperty("minAsDate")).toBe(null); - expect(await element.getProperty("maxAsDate")).toBe(null); + const element = await page.find("calcite-date-picker"); - const dateBeyondMax = "2022-11-26"; - await setActiveDate(page, dateBeyondMax); - expect(await getActiveDate(page)).toEqual(new Date(dateBeyondMax).toISOString()); + element.setProperty("min", null); + element.setProperty("max", null); + await page.waitForChanges(); - const dateBeforeMin = "2022-11-14"; - await setActiveDate(page, dateBeforeMin); - expect(await getActiveDate(page)).toEqual(new Date(dateBeforeMin).toISOString()); - }); + expect(await element.getProperty("minAsDate")).toBe(null); + expect(await element.getProperty("maxAsDate")).toBe(null); - it("passes down the default year prop to child date-picker-month-header", async () => { - const page = await newE2EPage(); - await page.setContent(html``); - const date = await page.find(`calcite-date-picker >>> calcite-date-picker-month-header`); + const dateBeyondMax = "2022-11-26"; + await setActiveDate(page, dateBeyondMax); + expect(await getActiveDate(page)).toEqual(new Date(dateBeyondMax).toISOString()); - expect(await date.getProperty("messages")).toEqual({ - nextMonth: "Next month", - prevMonth: "Previous month", - monthMenu: "Month menu", - yearMenu: "Year menu", - year: "Year", + const dateBeforeMin = "2022-11-14"; + await setActiveDate(page, dateBeforeMin); + expect(await getActiveDate(page)).toEqual(new Date(dateBeforeMin).toISOString()); }); }); @@ -373,7 +273,8 @@ describe("calcite-date-picker", () => { await page.waitForChanges(); await page.keyboard.press("Tab"); await page.waitForChanges(); - + await page.keyboard.press("Tab"); + await page.waitForChanges(); await page.keyboard.press("ArrowUp"); await page.waitForChanges(); await page.keyboard.press("Enter"); @@ -417,6 +318,8 @@ describe("calcite-date-picker", () => { await page.waitForChanges(); await page.keyboard.press("Tab"); await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); await page.keyboard.press("ArrowUp"); await page.waitForChanges(); await page.keyboard.press("Enter"); @@ -465,7 +368,6 @@ describe("calcite-date-picker", () => { await page.waitForChanges(); await page.keyboard.press("Tab"); await page.waitForChanges(); - await page.keyboard.press("ArrowUp"); await page.waitForChanges(); await page.keyboard.press("Enter"); @@ -477,7 +379,6 @@ describe("calcite-date-picker", () => { await page.waitForChanges(); await page.keyboard.press("Enter"); await page.waitForChanges(); - await page.waitForTimeout(4000); expect(await datePicker.getProperty("value")).toEqual(["2023-11-25", "2023-12-25"]); await page.keyboard.press("PageDown"); @@ -486,7 +387,6 @@ describe("calcite-date-picker", () => { await page.waitForChanges(); await page.keyboard.press("Enter"); await page.waitForChanges(); - await page.waitForTimeout(4000); expect(await datePicker.getProperty("value")).toEqual(["2023-11-25", "2024-01-25"]); await page.keyboard.press("ArrowDown"); @@ -524,7 +424,6 @@ describe("calcite-date-picker", () => { await page.waitForChanges(); await page.keyboard.press("Enter"); await page.waitForChanges(); - expect(await datePicker.getProperty("value")).toEqual(["2023-12-25", "2024-01-08"]); await page.keyboard.press("PageUp"); @@ -544,26 +443,81 @@ describe("calcite-date-picker", () => { }); }); - it("restarts range on selection after a range is complete when proximitySelectionDisabled is set", async () => { - const page = await newE2EPage(); - await page.setContent( - html` `, - ); - const datePicker = await page.find("calcite-date-picker"); + describe("hover range", () => { + it("should toggle range-hover attribute when updating the range", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + const datePicker = await page.find("calcite-date-picker"); + datePicker.setProperty("value", ["2024-01-01", "2024-02-10"]); - await selectDay("20200908", page, "mouse"); - await page.waitForChanges(); - await selectDay("20200923", page, "mouse"); - await page.waitForChanges(); - expect(await datePicker.getProperty("value")).toEqual(["2020-09-08", "2020-09-23"]); + await page.waitForChanges(); + let dateInsideRange = await getDayById(page, "20240109"); + await dateInsideRange.hover(); + await page.waitForChanges(); + expect(await (await getDayById(page, "20240108")).getProperty("rangeHover")).toBe(true); + expect(await (await getDayById(page, "20240208")).getProperty("rangeHover")).toBe(false); - await selectDay("20200915", page, "mouse"); - await page.waitForChanges(); - expect(await datePicker.getProperty("value")).toEqual(["2020-09-15", ""]); + dateInsideRange = await getDayById(page, "20240205"); + await dateInsideRange.hover(); + expect(await (await getDayById(page, "20240108")).getProperty("rangeHover")).toBe(false); + expect(await (await getDayById(page, "20240208")).getProperty("rangeHover")).toBe(true); + }); - await selectDay("20200930", page, "mouse"); - await page.waitForChanges(); - expect(await datePicker.getProperty("value")).toEqual(["2020-09-15", "2020-09-30"]); + it("should add range-hover attribute to dates falls outside of current range when extending", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + const datePicker = await page.find("calcite-date-picker"); + datePicker.setProperty("value", ["2024-01-05", "2024-02-15"]); + + await page.waitForChanges(); + expect(await (await getDayById(page, "20240108")).getProperty("rangeHover")).toBe(false); + expect(await (await getDayById(page, "20240208")).getProperty("rangeHover")).toBe(false); + + let dateInsideRange = await getDayById(page, "20240101"); + await dateInsideRange.hover(); + await page.waitForChanges(); + expect(await (await getDayById(page, "20240102")).getProperty("rangeHover")).toBe(true); + expect(await (await getDayById(page, "20240108")).getProperty("rangeHover")).toBe(false); + expect(await (await getDayById(page, "20240208")).getProperty("rangeHover")).toBe(false); + + dateInsideRange = await getDayById(page, "20240225"); + await dateInsideRange.hover(); + await page.waitForChanges(); + expect(await (await getDayById(page, "20240102")).getProperty("rangeHover")).toBe(false); + expect(await (await getDayById(page, "20240108")).getProperty("rangeHover")).toBe(false); + expect(await (await getDayById(page, "20240208")).getProperty("rangeHover")).toBe(false); + expect(await (await getDayById(page, "20240224")).getProperty("rangeHover")).toBe(true); + }); + + it("should not add range-hover attribute to dates before startDate and after endDate during initial selection", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + + await setActiveDate(page, "01-01-2024"); + await page.waitForChanges(); + + const startDate = await getDayById(page, "20240108"); + await startDate.hover(); + await page.waitForChanges(); + + await selectDayInMonthById("20240108", page); + await page.waitForChanges(); + expect(await (await getDayById(page, "20240107")).getProperty("rangeHover")).toBeFalsy(); + expect(await (await getDayById(page, "20240209")).getProperty("rangeHover")).toBeFalsy(); + + const endDate = await getDayById(page, "20240205"); + await endDate.hover(); + await page.waitForChanges(); + expect(await (await getDayById(page, "20240107")).getProperty("rangeHover")).toBeFalsy(); + expect(await (await getDayById(page, "20240209")).getProperty("rangeHover")).toBeFalsy(); + expect(await (await getDayById(page, "20240201")).getProperty("rangeHover")).toBe(true); + + await selectDayInMonthById("20240205", page); + await page.waitForChanges(); + expect(await (await getDayById(page, "20240107")).getProperty("rangeHover")).toBeFalsy(); + expect(await (await getDayById(page, "20240209")).getProperty("rangeHover")).toBeFalsy(); + expect(await (await getDayById(page, "20240201")).getProperty("rangeHover")).toBe(false); + }); }); describe("cross-century date values", () => { @@ -579,7 +533,7 @@ describe("calcite-date-picker", () => { expect(await datePicker.getProperty("value")).toBe(initialValue); const selectedDateInCentury = `${year}0307`; - await selectDay(selectedDateInCentury, page, "mouse"); + await selectDayInMonthById(selectedDateInCentury, page); await page.waitForChanges(); expect(await datePicker.getProperty("value")).toBe(`${year}-03-07`); @@ -617,6 +571,436 @@ describe("calcite-date-picker", () => { await assertCenturyDateValue(1750, "Europe/Zurich"); }); }); + + describe("month & year selection", () => { + it("should allow selecting last valid month from month select menu in start calendar", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + + await setActiveDate(page, "07-01-2024"); + await page.waitForChanges(); + + const [monthSelectStart, monthSelectEnd] = await page.findAll( + `calcite-date-picker >>> calcite-date-picker-month-header >>> calcite-select.${MONTH_HEADER_CSS.monthPicker}`, + ); + const [yearInputStart, yearInputEnd] = await page.findAll( + "calcite-date-picker >>> calcite-date-picker-month-header >>> input", + ); + expect(await yearInputStart.getProperty("value")).toBe("2024"); + expect(await yearInputEnd.getProperty("value")).toBe("2024"); + expect(await monthSelectStart.getProperty("value")).toBe("July"); + expect(await monthSelectEnd.getProperty("value")).toBe("August"); + + await monthSelectStart.click(); + await page.waitForChanges(); + + await page.select( + `calcite-date-picker >>> calcite-date-picker-month-header >>> calcite-select.${MONTH_HEADER_CSS.monthPicker} >>> select`, + "October", + ); + await page.waitForChanges(); + + expect(await monthSelectStart.getProperty("value")).toBe("October"); + expect(await monthSelectEnd.getProperty("value")).toBe("November"); + expect(await yearInputEnd.getProperty("value")).toBe("2024"); + expect(await yearInputStart.getProperty("value")).toBe("2024"); + }); + + it("should allow selecting first valid month from month select menu in end calendar", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + + await setActiveDate(page, "01-01-2024"); + await page.waitForChanges(); + + const [monthSelectStart, monthSelectEnd] = await page.findAll( + `calcite-date-picker >>> calcite-date-picker-month-header >>> calcite-select.${MONTH_HEADER_CSS.monthPicker}`, + ); + const [yearInputStart, yearInputEnd] = await page.findAll( + "calcite-date-picker >>> calcite-date-picker-month-header >>> input", + ); + expect(await yearInputStart.getProperty("value")).toBe("2024"); + expect(await yearInputEnd.getProperty("value")).toBe("2024"); + expect(await monthSelectStart.getProperty("value")).toBe("January"); + expect(await monthSelectEnd.getProperty("value")).toBe("February"); + + await monthSelectEnd.click(); + await page.waitForChanges(); + + await page.select( + `calcite-date-picker >>> [data-test-calendar="end"] >>> calcite-select.${MONTH_HEADER_CSS.monthPicker} >>> select`, + "January", + ); + await page.waitForChanges(); + expect(await monthSelectStart.getProperty("value")).toBe("December"); + expect(await yearInputEnd.getProperty("value")).toBe("2024"); + expect(await yearInputStart.getProperty("value")).toBe("2023"); + }); + }); + + it("should have current-day class for current day only", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + + const activeDate = await page.find( + "calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day[tabindex='0']", + ); + expect(activeDate.classList.contains(MONTH_CSS.currentDay)).toBe(true); + + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Enter"); + await page.waitForChanges(); + + const firstDayInPreviousMonth = await page.find( + "calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day[tabindex='0']", + ); + expect(firstDayInPreviousMonth.classList.contains(MONTH_CSS.currentDay)).toBe(false); + let currentDay = await page.find( + `calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day.${MONTH_CSS.currentDay}`, + ); + expect(currentDay).toBeTruthy(); + + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + + await page.keyboard.press("Enter"); + await page.waitForChanges(); + await page.keyboard.press("Enter"); + await page.waitForChanges(); + + const firstDayInNextMonth = await page.find( + "calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day[tabindex='0']", + ); + expect(firstDayInNextMonth.classList.contains(MONTH_CSS.currentDay)).toBe(false); + currentDay = await page.find( + `calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day.${MONTH_CSS.currentDay}`, + ); + expect(currentDay).toBeFalsy(); + }); + + describe("navigating months", () => { + it("correctly changes date on next/prev", async () => { + const page = await newE2EPage(); + await page.setContent(""); + + const [prevMonth, nextMonth] = await page.findAll( + `calcite-date-picker >>> calcite-date-picker-month-header >>> .${MONTH_HEADER_CSS.header} >>> calcite-action`, + ); + const monthSelect = await page.find("calcite-date-picker >>> calcite-select"); + const yearInput = await page.find("calcite-date-picker >>> input"); + + await prevMonth.click(); + await nextMonth.click(); + await nextMonth.click(); + await nextMonth.click(); + + const currentMonth = await monthSelect.getProperty("value"); + const currentYear = await yearInput.getProperty("value"); + expect(currentMonth).toBe("January"); + expect(currentYear).toBe("2001"); + }); + + it("should navigate to previous month from last valid month", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + + const nextMonth = await page.find( + `calcite-date-picker >>> calcite-date-picker-month-header >>> .${MONTH_HEADER_CSS.header} >>> calcite-action[aria-label='Next month']`, + ); + + await nextMonth.click(); + await page.waitForChanges(); + expect(await getActiveMonth(page)).toBe("October"); + + await nextMonth.click(); + await page.waitForChanges(); + expect(await getActiveMonth(page)).toBe("November"); + + await page.keyboard.press("Enter"); + await page.waitForChanges(); + expect(await getActiveMonth(page)).toBe("October"); + }); + + it("should navigate to next month from first valid month", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + + const prevMonth = await page.find( + `calcite-date-picker >>> calcite-date-picker-month-header >>> .${MONTH_HEADER_CSS.header} >>> calcite-action[aria-label='Previous month']`, + ); + + await prevMonth.click(); + await page.waitForChanges(); + expect(await getActiveMonth(page)).toBe("December"); + + await prevMonth.click(); + await page.waitForChanges(); + expect(await getActiveMonth(page)).toBe("November"); + + await page.keyboard.press("Enter"); + await page.waitForChanges(); + expect(await getActiveMonth(page)).toBe("December"); + }); + }); + + describe("selection", () => { + it("should select first date in month when max is before current in range", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await page.waitForChanges(); + const datePicker = await page.find("calcite-date-picker"); + + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Enter"); + await page.waitForChanges(); + + expect(await datePicker.getProperty("value")).toStrictEqual(["2024-07-01", ""]); + }); + + it("should select first date in month when max is before current", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await page.waitForChanges(); + const datePicker = await page.find("calcite-date-picker"); + + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Enter"); + await page.waitForChanges(); + + expect(await datePicker.getProperty("value")).toStrictEqual("2024-08-01"); + }); + + it("should select first valid date in month when minAsDate is after current in range", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await page.waitForChanges(); + const datePicker = await page.find("calcite-date-picker"); + + const currentDate = new Date(); + currentDate.setMonth(currentDate.getMonth() + 2); + currentDate.setDate(12); + const currentISODate = currentDate.toISOString().split("T")[0]; + + await page.evaluate((currentISODate) => { + const datePicker = document.querySelector("calcite-date-picker"); + datePicker.min = currentISODate; + }, currentISODate); + + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Enter"); + await page.waitForChanges(); + + expect(await datePicker.getProperty("value")).toStrictEqual([currentISODate, ""]); + }); + + it("should select current day when min is before current day but in same month of range date-picker", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await page.waitForChanges(); + const datePicker = await page.find("calcite-date-picker"); + + const currentDate = new Date(); + if (currentDate.getDate() > 2) { + currentDate.setDate(1); + } + const currentISODate = currentDate.toISOString().split("T")[0]; + + await page.evaluate((currentISODate) => { + const datePicker = document.querySelector("calcite-date-picker"); + datePicker.min = currentISODate; + }, currentISODate); + + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Enter"); + await page.waitForChanges(); + + const currentDayDate = new Date(); + const currentDayISODate = currentDayDate.toISOString().split("T")[0]; + + expect(await datePicker.getProperty("value")).toStrictEqual([currentDayISODate, ""]); + }); + + it("should select current day when min is before current day but in same month", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await page.waitForChanges(); + const datePicker = await page.find("calcite-date-picker"); + + const currentDate = new Date(); + if (currentDate.getDate() > 2) { + currentDate.setDate(1); + } + const currentISODate = currentDate.toISOString().split("T")[0]; + + await page.evaluate((currentISODate) => { + const datePicker = document.querySelector("calcite-date-picker"); + datePicker.min = currentISODate; + }, currentISODate); + + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Enter"); + await page.waitForChanges(); + + const currentDayDate = new Date(); + const currentDayISODate = currentDayDate.toISOString().split("T")[0]; + + expect(await datePicker.getProperty("value")).toEqual(currentDayISODate); + }); + + it("should select first valid date in month when minAsDate is after current", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await page.waitForChanges(); + const datePicker = await page.find("calcite-date-picker"); + + const currentDate = new Date(); + currentDate.setMonth(currentDate.getMonth() + 2); + currentDate.setDate(12); + const currentISODate = currentDate.toISOString().split("T")[0]; + + await page.evaluate((currentISODate) => { + const datePicker = document.querySelector("calcite-date-picker"); + datePicker.min = currentISODate; + }, currentISODate); + + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Enter"); + await page.waitForChanges(); + + expect(await datePicker.getProperty("value")).toEqual(currentISODate); + }); + }); + + it("updates the calendar immediately as a new year is typed but doesn't change the year", async () => { + const page = await newE2EPage(); + await page.setContent(``); + const datePicker = await page.find("calcite-date-picker"); + await skipAnimations(page); + + async function getActiveMonthDate(): Promise { + return page.$eval("calcite-date-picker", (datePicker: HTMLCalciteDatePickerElement) => + datePicker.shadowRoot.querySelector("calcite-date-picker-month").activeDate.toISOString(), + ); + } + + async function getActiveMonthHeaderInputValue(): Promise { + return page.$eval( + "calcite-date-picker", + (datePicker: HTMLCalciteDatePickerElement) => + datePicker.shadowRoot + .querySelector("calcite-date-picker-month") + .shadowRoot.querySelector("calcite-date-picker-month-header") + .shadowRoot.querySelector(".year").value, + ); + } + + const activeDateBefore = await getActiveMonthDate(); + + await page.keyboard.press("Tab"); + await page.keyboard.press("Tab"); + await page.keyboard.down("Meta"); + await page.keyboard.press("a"); + expect(await getActiveMonthHeaderInputValue()).toBe("2015"); + await page.keyboard.press("Backspace"); + await page.keyboard.up("Meta"); + await page.keyboard.type("2016"); + await page.waitForChanges(); + expect(await getActiveMonthHeaderInputValue()).toBe("2016"); + + const activeDateAfter = await getActiveMonthDate(); + + expect(activeDateBefore).not.toEqual(activeDateAfter); + expect(await datePicker.getProperty("value")).toBe("2015-02-28"); + }); + + it("passes down the default year prop to child date-picker-month-header", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + const date = await page.find(`calcite-date-picker >>> calcite-date-picker-month`); + + expect(await date.getProperty("messages")).toEqual({ + nextMonth: "Next month", + prevMonth: "Previous month", + monthMenu: "Month menu", + yearMenu: "Year menu", + year: "Year", + }); + }); + + it("restarts range on selection after a range is complete when proximitySelectionDisabled is set", async () => { + const page = await newE2EPage(); + await page.setContent( + html` `, + ); + const datePicker = await page.find("calcite-date-picker"); + + await selectDayInMonthById("20200908", page); + await page.waitForChanges(); + await selectDayInMonthById("20200923", page); + await page.waitForChanges(); + expect(await datePicker.getProperty("value")).toEqual(["2020-09-08", "2020-09-23"]); + + await selectDayInMonthById("20200915", page); + await page.waitForChanges(); + expect(await datePicker.getProperty("value")).toEqual(["2020-09-15", ""]); + + await selectDayInMonthById("20200930", page); + await page.waitForChanges(); + expect(await datePicker.getProperty("value")).toEqual(["2020-09-15", "2020-09-30"]); + }); }); async function setActiveDate(page: E2EPage, date: string): Promise { @@ -633,3 +1017,42 @@ async function getActiveDate(page: E2EPage): Promise { return datePicker.activeDate.toISOString(); }); } + +async function selectDayInMonthById(id: string, page: E2EPage): Promise { + const day = await page.find( + `calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day[current-month][id="${id}"]`, + ); + await day.click(); + await page.waitForChanges(); +} + +async function selectFirstAvailableDay(page: E2EPage): Promise { + const day = await page.find( + "calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day:not([selected])", + ); + await day.click(); + await page.waitForChanges(); +} + +async function selectSelectedDay(page: E2EPage): Promise { + const day = await page.find( + "calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day[selected]", + ); + await day.click(); + await page.waitForChanges(); +} + +async function getDayById(page: E2EPage, id: string): Promise { + return await page.find(`calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day[id="${id}"]`); +} + +async function getActiveMonth(page: E2EPage, position: Extract<"start" | "end", Position> = "start"): Promise { + const [startMonth, endMonth] = await page.findAll( + `calcite-date-picker >>> calcite-date-picker-month-header >>> .${MONTH_HEADER_CSS.header} >>> calcite-select.${MONTH_HEADER_CSS.monthPicker}`, + ); + + if (position === "start") { + return (await startMonth.find("calcite-option[selected]")).textContent; + } + return (await endMonth.find("calcite-option[selected]")).textContent; +} diff --git a/packages/calcite-components/src/components/date-picker/date-picker.scss b/packages/calcite-components/src/components/date-picker/date-picker.scss index c94b0112c41..783823950e7 100644 --- a/packages/calcite-components/src/components/date-picker/date-picker.scss +++ b/packages/calcite-components/src/components/date-picker/date-picker.scss @@ -7,25 +7,43 @@ inline-block overflow-visible rounded-none - w-auto; + w-full; } :host([scale="s"]) { - inline-size: 234px; + inline-size: 236px; min-inline-size: 216px; max-inline-size: 380px; } +:host([scale="s"][range][layout="horizontal"]) { + inline-size: 480px; + min-inline-size: 432px; + max-inline-size: 772px; +} + :host([scale="m"]) { - inline-size: 304px; + inline-size: 298px; min-inline-size: 272px; max-inline-size: 480px; } +:host([scale="m"][range][layout="horizontal"]) { + inline-size: 608px; + min-inline-size: 544px; + max-inline-size: 972px; +} + :host([scale="l"]) { - inline-size: 370px; + inline-size: 334px; min-inline-size: 320px; max-inline-size: 600px; } +:host([scale="l"][range][layout="horizontal"]) { + inline-size: 684px; + min-inline-size: 640px; + max-inline-size: 1212px; +} + @include base-component(); diff --git a/packages/calcite-components/src/components/date-picker/date-picker.stories.ts b/packages/calcite-components/src/components/date-picker/date-picker.stories.ts index 0f034e41de1..c9cd30325fe 100644 --- a/packages/calcite-components/src/components/date-picker/date-picker.stories.ts +++ b/packages/calcite-components/src/components/date-picker/date-picker.stories.ts @@ -83,6 +83,36 @@ export const rangeHighlighted_TestOnly = (): string => html` `; +export const rangeValuesNotInSameMonthAndYear_TestOnly = (): string => html` +
+ +
+ +`; + +export const Focus = (): string => html` +
+ +
+ +`; + +Focus.parameters = { + chromatic: { delay: 2000 }, +}; + export const rangeRTL_TestOnly = (): string => html`
diff --git a/packages/calcite-components/src/components/date-picker/date-picker.tsx b/packages/calcite-components/src/components/date-picker/date-picker.tsx index 179ed394473..fa3e2065a37 100644 --- a/packages/calcite-components/src/components/date-picker/date-picker.tsx +++ b/packages/calcite-components/src/components/date-picker/date-picker.tsx @@ -16,8 +16,12 @@ import { dateFromRange, dateToISO, getDaysDiff, + getFirstValidDateInMonth, HoverRange, - setEndOfDay, + inRange, + nextMonth, + prevMonth, + sameDate, } from "../../utils/date"; import { componentFocusable, @@ -42,6 +46,7 @@ import { } from "../../utils/t9n"; import { HeadingLevel } from "../functional/Heading"; import { isBrowser } from "../../utils/browser"; +import { focusFirstTabbable } from "../../utils/dom"; import { DatePickerMessages } from "./assets/date-picker/t9n"; import { DATE_PICKER_FORMAT_OPTIONS, HEADING_LEVEL } from "./resources"; import { DateLocaleData, getLocaleData, getValueAsDateRange } from "./utils"; @@ -65,9 +70,18 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom @Prop({ mutable: true }) activeDate: Date; @Watch("activeDate") - activeDateWatcher(newActiveDate: Date): void { - if (this.activeRange === "end") { - this.activeEndDate = newActiveDate; + activeDateWatcher(newValue: Date): void { + if (!this.range) { + return; + } + + if (!this.rangeValueChangedByUser) { + if (newValue) { + this.activeStartDate = newValue; + this.activeEndDate = nextMonth(this.activeStartDate); + } else { + this.resetActiveDates(); + } } } @@ -81,22 +95,33 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom */ @Prop({ mutable: true }) value: string | string[]; + @Watch("value") + valueHandler(value: string | string[]): void { + if (Array.isArray(value)) { + this.valueAsDate = getValueAsDateRange(value); + if (!this.rangeValueChangedByUser) { + this.resetActiveDates(); + } + } else if (value) { + this.valueAsDate = dateFromISO(value); + } + } + /** * Specifies the heading level of the component's `heading` for proper document structure, without affecting visual styling. */ @Prop({ reflect: true }) headingLevel: HeadingLevel; + /** Defines the layout of the component. */ + @Prop({ reflect: true }) layout: "horizontal" | "vertical" = "horizontal"; + /** Specifies the selected date as a full date object (`new Date("yyyy-mm-dd")`), or an array containing full date objects (`[new Date("yyyy-mm-dd"), new Date("yyyy-mm-dd")]`). */ @Prop({ mutable: true }) valueAsDate: Date | Date[]; @Watch("valueAsDate") valueAsDateWatcher(newValueAsDate: Date | Date[]): void { - if (this.range && Array.isArray(newValueAsDate)) { - const { activeStartDate, activeEndDate } = this; - const newActiveStartDate = newValueAsDate[0]; - const newActiveEndDate = newValueAsDate[1]; - this.activeStartDate = activeStartDate !== newActiveStartDate && newActiveStartDate; - this.activeEndDate = activeEndDate !== newActiveEndDate && newActiveEndDate; + if (this.range && Array.isArray(newValueAsDate) && !this.rangeValueChangedByUser) { + this.setActiveStartAndEndDates(); } else if (newValueAsDate && newValueAsDate !== this.activeDate) { this.activeDate = newValueAsDate as Date; } @@ -114,6 +139,9 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom @Watch("min") onMinChanged(min: string): void { this.minAsDate = dateFromISO(min); + if (this.range) { + this.setActiveStartAndEndDates(); + } } /** Specifies the latest allowed date (`"yyyy-mm-dd"`). */ @@ -122,8 +150,16 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom @Watch("max") onMaxChanged(max: string): void { this.maxAsDate = dateFromISO(max); + if (this.range) { + this.setActiveStartAndEndDates(); + } } + /** + * Specifies the monthStyle used by the component. + */ + @Prop() monthStyle: "abbreviated" | "wide" = "wide"; + /** * Specifies the Unicode numeral system used by the component for localization. This property cannot be dynamically changed. * @@ -183,7 +219,7 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom @Method() async setFocus(): Promise { await componentFocusable(this); - this.el.focus(); + focusFirstTabbable(this.el); } /** @@ -193,7 +229,7 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom @Method() async reset(): Promise { this.resetActiveDates(); - this.mostRecentRangeValue = undefined; + this.rangeValueChangedByUser = false; } // -------------------------------------------------------------------------- @@ -201,10 +237,10 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom // Lifecycle // // -------------------------------------------------------------------------- + connectedCallback(): void { connectLocalized(this); connectMessages(this); - if (Array.isArray(this.value)) { this.valueAsDate = getValueAsDateRange(this.value); } else if (this.value) { @@ -218,6 +254,7 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom if (this.max) { this.maxAsDate = dateFromISO(this.max); } + this.setActiveStartAndEndDates(); } disconnectedCallback(): void { @@ -243,39 +280,24 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom this.minAsDate, this.maxAsDate, ); - let activeDate = this.getActiveDate(date, this.minAsDate, this.maxAsDate); + const activeDate = this.getActiveDate(date, this.minAsDate, this.maxAsDate); const endDate = this.range && Array.isArray(this.valueAsDate) ? dateFromRange(this.valueAsDate[1], this.minAsDate, this.maxAsDate) : null; - const activeEndDate = this.getActiveEndDate(endDate, this.minAsDate, this.maxAsDate); - if ( - (this.activeRange === "end" || - (this.hoverRange?.focused === "end" && (!this.proximitySelectionDisabled || endDate))) && - activeEndDate - ) { - activeDate = activeEndDate; - } - if (this.range && this.mostRecentRangeValue) { - activeDate = this.mostRecentRangeValue; - } const minDate = this.range && this.activeRange ? this.activeRange === "start" ? this.minAsDate - : date || this.minAsDate + : date : this.minAsDate; - const maxDate = - this.range && this.activeRange - ? this.activeRange === "start" - ? endDate || this.maxAsDate - : this.maxAsDate - : this.maxAsDate; + const startCalendarActiveDate = this.range ? this.activeStartDate : activeDate; + return ( - - {this.renderCalendar(activeDate, maxDate, minDate, date, endDate)} + + {this.renderMonth(startCalendarActiveDate, this.maxAsDate, minDate, date, endDate)} ); } @@ -320,10 +342,10 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom @State() private localeData: DateLocaleData; - @State() private mostRecentRangeValue?: Date; - @State() startAsDate: Date; + private rangeValueChangedByUser = false; + //-------------------------------------------------------------------------- // // Private Methods @@ -336,15 +358,6 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom } }; - @Watch("value") - valueHandler(value: string | string[]): void { - if (Array.isArray(value)) { - this.valueAsDate = getValueAsDateRange(value); - } else if (value) { - this.valueAsDate = dateFromISO(value); - } - } - @Watch("effectiveLocale") private async loadLocaleData(): Promise { if (!isBrowser()) { @@ -361,40 +374,54 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom this.dateTimeFormat = getDateTimeFormat(this.effectiveLocale, DATE_PICKER_FORMAT_OPTIONS); } - monthHeaderSelectChange = (event: CustomEvent): void => { - const date = new Date(event.detail); + private monthHeaderSelectChange = ( + event: CustomEvent<{ date: Date; position: string }>, + ): void => { + const date = new Date(event.detail.date); + const position = event.detail.position; if (!this.range) { this.activeDate = date; } else { - if (this.activeRange === "end") { + if (position === "end") { this.activeEndDate = date; + this.activeStartDate = prevMonth(date); } else { this.activeStartDate = date; + this.activeEndDate = nextMonth(date); } - this.mostRecentRangeValue = date; } + event.stopPropagation(); }; - monthActiveDateChange = (event: CustomEvent): void => { + private monthActiveDateChange = (event: CustomEvent): void => { const date = new Date(event.detail); if (!this.range) { this.activeDate = date; } else { + const month = date.getMonth(); + const isDateOutOfCurrentRange = + month !== this.activeStartDate.getMonth() && + month !== nextMonth(this.activeStartDate).getMonth(); if (this.activeRange === "end") { - this.activeEndDate = date; + if (!this.activeEndDate || (this.activeStartDate && isDateOutOfCurrentRange)) { + this.activeEndDate = date; + this.activeStartDate = prevMonth(date); + } } else { - this.activeStartDate = date; + if ((this.activeStartDate && isDateOutOfCurrentRange) || !this.activeStartDate) { + this.activeStartDate = date; + this.activeEndDate = nextMonth(date); + } } - this.mostRecentRangeValue = date; } + event.stopPropagation(); }; - monthHoverChange = (event: CustomEvent): void => { + private monthHoverChange = (event: CustomEvent): void => { if (!this.range) { this.hoverRange = undefined; return; } - const { valueAsDate } = this; const start = Array.isArray(valueAsDate) && valueAsDate[0]; const end = Array.isArray(valueAsDate) && valueAsDate[1]; @@ -420,21 +447,31 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom this.hoverRange = undefined; } } else { - if (start && end) { - const startDiff = getDaysDiff(date, start); - const endDiff = getDaysDiff(date, end); - if (endDiff > 0) { + if (this.activeRange) { + if (this.activeRange === "end") { this.hoverRange.end = date; this.hoverRange.focused = "end"; - } else if (startDiff < 0) { - this.hoverRange.start = date; - this.hoverRange.focused = "start"; - } else if (startDiff > endDiff) { + } else { this.hoverRange.start = date; this.hoverRange.focused = "start"; - } else { + } + } else if (start && end) { + const startDiff = Math.abs(getDaysDiff(date, start)); + const endDiff = Math.abs(getDaysDiff(date, end)); + if (date > end) { this.hoverRange.end = date; this.hoverRange.focused = "end"; + } else if (date < start) { + this.hoverRange.start = date; + this.hoverRange.focused = "start"; + } else if (date > start && date < end) { + if (startDiff < endDiff) { + this.hoverRange.start = date; + this.hoverRange.focused = "start"; + } else { + this.hoverRange.end = date; + this.hoverRange.focused = "end"; + } } } else { if (start) { @@ -454,10 +491,11 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom event.stopPropagation(); }; - monthMouseOutChange = (): void => { + monthMouseOutChange = (event: CustomEvent): void => { if (this.hoverRange) { this.hoverRange = undefined; } + event.stopPropagation(); }; /** @@ -469,43 +507,38 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom * @param date * @param endDate */ - private renderCalendar( + private renderMonth( activeDate: Date, maxDate: Date, minDate: Date, date: Date, endDate: Date, - ) { + ): VNode { return ( - this.localeData && [ - , + this.localeData && ( , - ] + /> + ) ); } @@ -524,6 +557,7 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom this.activeEndDate = new Date(valueAsDate[1]); } } + this.hoverRange = undefined; }; private getEndDate(): Date { @@ -532,12 +566,12 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom private setEndDate(date: Date): void { const startDate = this.getStartDate(); - const newEndDate = date ? setEndOfDay(date) : date; + this.rangeValueChangedByUser = true; this.value = [dateToISO(startDate), dateToISO(date)]; this.valueAsDate = [startDate, date]; - this.mostRecentRangeValue = newEndDate; - this.calciteDatePickerRangeChange.emit(); - this.activeEndDate = date || null; + if (date) { + this.calciteDatePickerRangeChange.emit(); + } } private getStartDate(): Date { @@ -546,11 +580,10 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom private setStartDate(date: Date): void { const endDate = this.getEndDate(); + this.rangeValueChangedByUser = true; this.value = [dateToISO(date), dateToISO(endDate)]; this.valueAsDate = [date, endDate]; - this.mostRecentRangeValue = date; this.calciteDatePickerRangeChange.emit(); - this.activeStartDate = date || null; } /** @@ -597,6 +630,11 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom if (this.activeRange == "end") { this.setEndDate(date); } else { + //allows start end to go beyond end date and set the end date to empty while editing + if (date > end) { + this.setEndDate(null); + this.activeEndDate = null; + } this.setStartDate(date); } } else { @@ -614,6 +652,7 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom } } } + event.stopPropagation(); this.calciteDatePickerChange.emit(); }; @@ -625,12 +664,55 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom * @param max */ private getActiveDate(value: Date | null, min: Date | null, max: Date | null): Date { - return dateFromRange(this.activeDate, min, max) || value || dateFromRange(new Date(), min, max); + const activeDate = dateFromRange(new Date(), min, max); + + return ( + dateFromRange(this.activeDate, min, max) || + value || + (sameDate(max, activeDate) && !this.range + ? getFirstValidDateInMonth(activeDate, min, max) + : activeDate) + ); } private getActiveEndDate(value: Date | null, min: Date | null, max: Date | null): Date { return ( - dateFromRange(this.activeEndDate, min, max) || value || dateFromRange(new Date(), min, max) + dateFromRange(this.activeEndDate, min, max) || + value || + dateFromRange(nextMonth(new Date()), min, max) ); } + + private setActiveStartAndEndDates(): void { + if (this.range) { + const startDate = dateFromRange( + Array.isArray(this.valueAsDate) ? this.valueAsDate[0] : this.valueAsDate, + this.minAsDate, + this.maxAsDate, + ); + + const endDate = dateFromRange( + Array.isArray(this.valueAsDate) ? this.valueAsDate[1] : null, + this.minAsDate, + this.maxAsDate, + ); + + this.activeStartDate = this.getActiveDate(startDate, this.minAsDate, this.maxAsDate); + this.activeEndDate = this.getActiveEndDate(endDate, this.minAsDate, this.maxAsDate); + + if (sameDate(this.activeStartDate, this.activeEndDate)) { + const previousMonthActiveDate = getFirstValidDateInMonth( + prevMonth(this.activeEndDate), + this.minAsDate, + this.maxAsDate, + ); + const nextMonthActiveDate = nextMonth(this.activeEndDate); + if (inRange(previousMonthActiveDate, this.minAsDate, this.maxAsDate)) { + this.activeStartDate = previousMonthActiveDate; + } else if (inRange(nextMonthActiveDate, this.minAsDate, this.maxAsDate)) { + this.activeEndDate = nextMonthActiveDate; + } + } + } + } } diff --git a/packages/calcite-components/src/components/input-date-picker/input-date-picker.e2e.ts b/packages/calcite-components/src/components/input-date-picker/input-date-picker.e2e.ts index fc18d94e22a..fbe6b4b898a 100644 --- a/packages/calcite-components/src/components/input-date-picker/input-date-picker.e2e.ts +++ b/packages/calcite-components/src/components/input-date-picker/input-date-picker.e2e.ts @@ -13,8 +13,9 @@ import { focusable, } from "../../tests/commonTests"; import { html } from "../../../support/formatting"; +import { getFocusedElementProp, isElementFocused, skipAnimations } from "../../tests/utils"; +import { Position } from "../interfaces"; import { CSS as MONTH_HEADER_CSS } from "../date-picker-month-header/resources"; -import { getFocusedElementProp, skipAnimations } from "../../tests/utils"; import { CSS } from "./resources"; const animationDurationInMs = 200; @@ -66,12 +67,7 @@ describe("calcite-input-date-picker", () => { }); describe("disabled", () => { - disabled("calcite-input-date-picker", { - focusTarget: { - tab: "calcite-input-date-picker", - click: "calcite-input-date-picker", - }, - }); + disabled("calcite-input-date-picker"); }); describe("openClose", () => { @@ -88,64 +84,6 @@ describe("calcite-input-date-picker", () => { }); }); - async function navigateMonth(page: E2EPage, direction: "previous" | "next"): Promise { - const linkIndex = direction === "previous" ? 0 : 1; - - await page.evaluate( - async (MONTH_HEADER_CSS, linkIndex: number): Promise => - document - .querySelector("calcite-input-date-picker") - .shadowRoot.querySelector("calcite-date-picker") - .shadowRoot.querySelector("calcite-date-picker-month-header") - .shadowRoot.querySelectorAll(`.${MONTH_HEADER_CSS.chevron}`) - [linkIndex].click(), - MONTH_HEADER_CSS, - linkIndex, - ); - await page.waitForChanges(); - } - - async function selectDayInMonth(page: E2EPage, day: number): Promise { - const dayIndex = day - 1; - - await page.evaluate( - async (dayIndex: number) => - document - .querySelector("calcite-input-date-picker") - .shadowRoot.querySelector("calcite-date-picker") - .shadowRoot.querySelector("calcite-date-picker-month") - .shadowRoot.querySelectorAll("calcite-date-picker-day[current-month]") - [dayIndex].click(), - dayIndex, - ); - await page.waitForChanges(); - } - - async function getActiveMonth(page: E2EPage): Promise { - return page.evaluate( - async (MONTH_HEADER_CSS) => - document - .querySelector("calcite-input-date-picker") - .shadowRoot.querySelector("calcite-date-picker") - .shadowRoot.querySelector("calcite-date-picker-month-header") - .shadowRoot.querySelector(`.${MONTH_HEADER_CSS.month}`).textContent, - MONTH_HEADER_CSS, - ); - } - - async function getDateInputValue(page: E2EPage, type: "start" | "end" = "start"): Promise { - const inputIndex = type === "start" ? 0 : 1; - - return page.evaluate( - async (inputIndex: number): Promise => - document - .querySelector("calcite-input-date-picker") - .shadowRoot.querySelectorAll("calcite-input-text") - [inputIndex].shadowRoot.querySelector("input").value, - inputIndex, - ); - } - describe("event emitting when the value changes", () => { it("emits change event when value is committed for single date", async () => { const page = await newE2EPage(); @@ -159,13 +97,8 @@ describe("calcite-input-date-picker", () => { await input.click(); await page.waitForChanges(); await page.waitForTimeout(animationDurationInMs); - const wrapper = await page.waitForFunction( - (calendarWrapperClass: string) => - document.querySelector("calcite-input-date-picker").shadowRoot.querySelector(`.${calendarWrapperClass}`), - {}, - CSS.calendarWrapper, - ); - expect(await wrapper.isIntersectingViewport()).toBe(true); + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + expect(await calendar.isVisible()).toBe(true); await page.keyboard.type("3/7/2020"); await page.keyboard.press("Enter"); @@ -224,21 +157,16 @@ describe("calcite-input-date-picker", () => { it("emits when value is committed for date range", async () => { const page = await newE2EPage(); await page.setContent(""); - const input = await page.find("calcite-input-date-picker"); + const inputDatePicker = await page.find("calcite-input-date-picker"); + const input = await page.find("calcite-input-date-picker >>> calcite-input-text"); const changeEvent = await page.spyOnEvent("calciteInputDatePickerChange"); await input.click(); await page.waitForChanges(); await page.waitForTimeout(animationDurationInMs); - const wrapper = await page.waitForFunction( - (calendarWrapperClass: string) => - document.querySelector("calcite-input-date-picker").shadowRoot.querySelector(`.${calendarWrapperClass}`), - {}, - CSS.calendarWrapper, - ); - - expect(await wrapper.isIntersectingViewport()).toBe(true); + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + expect(await calendar.isVisible()).toBe(true); const inputtedStartDate = "1/1/2020"; const expectedStartDateComponentValue = "2020-01-01"; @@ -250,14 +178,15 @@ describe("calcite-input-date-picker", () => { await page.keyboard.press("Enter"); await page.waitForChanges(); - expect(await input.getProperty("value")).toEqual([expectedStartDateComponentValue, ""]); + expect(await inputDatePicker.getProperty("value")).toEqual([expectedStartDateComponentValue, ""]); expect(changeEvent).toHaveReceivedEventTimes(1); + expect(await calendar.isVisible()).toBe(true); await page.keyboard.type(inputtedEndDate); await page.keyboard.press("Enter"); await page.waitForChanges(); - expect(await input.getProperty("value")).toEqual([ + expect(await inputDatePicker.getProperty("value")).toEqual([ expectedStartDateComponentValue, expectedEndDateComponentValue, ]); @@ -276,7 +205,7 @@ describe("calcite-input-date-picker", () => { await page.keyboard.press("Enter"); await page.waitForChanges(); - expect(await input.getProperty("value")).toEqual([expectedStartDateComponentValue, ""]); + expect(await inputDatePicker.getProperty("value")).toEqual([expectedStartDateComponentValue, ""]); expect(changeEvent).toHaveReceivedEventTimes(3); }); @@ -313,7 +242,7 @@ describe("calcite-input-date-picker", () => { await inputDatePickerEl.click(); await page.waitForChanges(); - await selectDayInMonth(page, 28); + await selectDayInMonthByIndex(page, 28); await page.waitForChanges(); expect(await inputDatePickerEl.getProperty("value")).toEqual("2023-06-28"); @@ -372,25 +301,20 @@ describe("calcite-input-date-picker", () => { }); it("toggles the date picker when clicked", async () => { - let calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); - + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); expect(await calendar.isVisible()).toBe(false); await inputDatePicker.click(); await page.waitForChanges(); - calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); - expect(await calendar.isVisible()).toBe(true); await inputDatePicker.click(); await page.waitForChanges(); - calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); - expect(await calendar.isVisible()).toBe(false); }); it("toggles the date picker when using arrow down/escape key", async () => { - let calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); expect(await calendar.isVisible()).toBe(false); @@ -398,14 +322,26 @@ describe("calcite-input-date-picker", () => { await page.waitForChanges(); await page.keyboard.press("ArrowDown"); await page.waitForChanges(); - calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); - expect(await calendar.isVisible()).toBe(true); await page.keyboard.press("Escape"); await page.waitForChanges(); - calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + expect(await calendar.isVisible()).toBe(false); + }); + + it("toggles the date picker when using arrow up/escape key", async () => { + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + + expect(await calendar.isVisible()).toBe(false); + + await inputDatePicker.callMethod("setFocus"); + await page.waitForChanges(); + await page.keyboard.press("ArrowUp"); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + await page.keyboard.press("Escape"); + await page.waitForChanges(); expect(await calendar.isVisible()).toBe(false); }); }); @@ -419,11 +355,6 @@ describe("calcite-input-date-picker", () => { inputDatePicker = await page.find("calcite-input-date-picker"); }); - async function isCalendarVisible(calendar: E2EElement, type: "start" | "end"): Promise { - const calendarPosition = calendar.classList.contains(CSS.calendarWrapperEnd) ? "end" : "start"; - return (await calendar.isVisible()) && calendarPosition === type; - } - async function resetFocus(page: E2EPage): Promise { await page.mouse.click(0, 0); } @@ -431,15 +362,11 @@ describe("calcite-input-date-picker", () => { it("toggles the date picker when clicked", async () => { const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); - expect(await isCalendarVisible(calendar, "start")).toBe(false); - expect(await isCalendarVisible(calendar, "end")).toBe(false); + expect(await calendar.isVisible()).toBe(false); const startInput = await page.find( `calcite-input-date-picker >>> .${CSS.inputWrapper}[data-position="start"] calcite-input-text`, ); - const startInputToggle = await page.find( - `calcite-input-date-picker >>> .${CSS.inputWrapper}[data-position="start"] .${CSS.toggleIcon}`, - ); const endInput = await page.find( `calcite-input-date-picker >>> .${CSS.inputWrapper}[data-position="end"] calcite-input-text`, @@ -449,200 +376,147 @@ describe("calcite-input-date-picker", () => { ); // toggling via start date input - await resetFocus(page); await startInput.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(true); + expect(await calendar.isVisible()).toBe(true); await startInput.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(false); - - // toggling via start date toggle icon - - await resetFocus(page); - await startInputToggle.click(); - await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(true); - - await startInputToggle.click(); - await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(false); + expect(await calendar.isVisible()).toBe(false); // toggling via end date input - await resetFocus(page); await endInput.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(true); + expect(await calendar.isVisible()).toBe(true); await endInput.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(false); + expect(await calendar.isVisible()).toBe(false); // toggling via end date toggle icon - await resetFocus(page); await endInputToggle.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(true); + expect(await calendar.isVisible()).toBe(true); await endInputToggle.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(false); - - // toggling via start date input and toggle icon - - await resetFocus(page); - await startInput.click(); - await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(true); - - await startInputToggle.click(); - await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(false); - - // toggling via start toggle icon and date input - - await resetFocus(page); - await startInputToggle.click(); - await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(true); - - await startInput.click(); - await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(false); + expect(await calendar.isVisible()).toBe(false); // toggling via end date input and toggle icon - await resetFocus(page); await endInput.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(true); + expect(await calendar.isVisible()).toBe(true); await endInputToggle.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(false); + expect(await calendar.isVisible()).toBe(false); // toggling via end toggle icon and date input - await resetFocus(page); await endInputToggle.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(true); + expect(await calendar.isVisible()).toBe(true); await endInput.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(false); + expect(await calendar.isVisible()).toBe(false); // toggling via start date input and end toggle icon - await resetFocus(page); await startInput.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(true); + expect(await calendar.isVisible()).toBe(true); await endInputToggle.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(true); - - // toggling via start toggle icon and date input - - await resetFocus(page); - await startInputToggle.click(); - await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(true); - - await endInput.click(); - await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(true); - - // close - await endInput.click(); - await page.waitForChanges(); - - // toggling via end date input and start toggle icon - - await resetFocus(page); - await endInput.click(); - await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(true); - - await startInputToggle.click(); - await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(true); + expect(await calendar.isVisible()).toBe(true); // toggling via end toggle icon and start date input - await resetFocus(page); await endInputToggle.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(true); + expect(await calendar.isVisible()).toBe(true); await resetFocus(page); await startInput.click(); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(true); + expect(await calendar.isVisible()).toBe(true); }); it("toggles the date picker when using arrow down/escape key", async () => { const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); - - expect(await isCalendarVisible(calendar, "start")).toBe(false); - expect(await isCalendarVisible(calendar, "end")).toBe(false); + expect(await calendar.isVisible()).toBe(false); await inputDatePicker.callMethod("setFocus"); await page.waitForChanges(); await page.keyboard.press("ArrowDown"); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(true); + expect(await calendar.isVisible()).toBe(true); await page.keyboard.press("Escape"); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "start")).toBe(false); + expect(await calendar.isVisible()).toBe(false); await page.keyboard.press("Tab"); - await page.keyboard.press("ArrowDown"); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(true); + expect(await calendar.isVisible()).toBe(true); await page.keyboard.press("Escape"); await page.waitForChanges(); - - expect(await isCalendarVisible(calendar, "end")).toBe(false); + expect(await calendar.isVisible()).toBe(false); }); }); }); + describe("close after selection", () => { + it("should close the date picker after selecting a date", async () => { + const page = await newE2EPage(); + await page.setContent(html` `); + await skipAnimations(page); + await page.waitForChanges(); + const inputDatePicker = await page.find("calcite-input-date-picker"); + + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + expect(await calendar.isVisible()).toBe(false); + + await inputDatePicker.click(); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + + await selectDayInMonthByIndex(page, 30); + expect(await calendar.isVisible()).toBe(false); + }); + + it("should close the range date picker after selecting both dates", async () => { + const page = await newE2EPage(); + await page.setContent(html` `); + await skipAnimations(page); + await page.waitForChanges(); + + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + expect(await calendar.isVisible()).toBe(false); + + const startInput = await page.find( + `calcite-input-date-picker >>> .${CSS.inputWrapper}[data-position="start"] calcite-input-text`, + ); + + await startInput.click(); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + + await selectDayInMonthByIndex(page, 30); + expect(await calendar.isVisible()).toBe(true); + + await selectDayInMonthByIndex(page, 50); + expect(await calendar.isVisible()).toBe(false); + }); + }); + describe("localization", () => { it("renders arabic numerals while typing in the input when numbering-system is set to arab", async () => { const page = await newE2EPage(); @@ -718,7 +592,7 @@ describe("calcite-input-date-picker", () => { await inputDatePicker.click(); await calciteInputDatePickerOpenEvent; - await selectDayInMonth(page, 1); + await selectDayInMonthByIndex(page, 1); await inputDatePicker.callMethod("blur"); expect(await inputDatePicker.getProperty("value")).toBe("2023-05-01"); @@ -733,7 +607,7 @@ describe("calcite-input-date-picker", () => { await inputDatePicker.click(); await calciteInputDatePickerOpenEvent; - await selectDayInMonth(page, 1); + await selectDayInMonthByIndex(page, 1); await inputDatePicker.callMethod("blur"); expect(await inputDatePicker.getProperty("value")).toBe("2023-05-01"); @@ -751,7 +625,7 @@ describe("calcite-input-date-picker", () => { await inputDatePicker.click(); await calciteInputDatePickerOpenEvent; - await selectDayInMonth(page, 1); + await selectDayInMonthByIndex(page, 1); await inputDatePicker.callMethod("blur"); expect(await inputDatePicker.getProperty("value")).toBe("2023-05-01"); @@ -770,7 +644,7 @@ describe("calcite-input-date-picker", () => { await inputDatePicker.click(); await page.waitForChanges(); - await selectDayInMonth(page, 1); + await selectDayInMonthByIndex(page, 1); expect(await inputDatePicker.getProperty("value")).toBe("2023-01-01"); }); @@ -790,7 +664,7 @@ describe("calcite-input-date-picker", () => { await inputDatePicker.click(); await page.waitForChanges(); - await selectDayInMonth(page, 7); + await selectDayInMonthByIndex(page, 7); expect(await inputDatePicker.getProperty("value")).toBe(`${year}-03-07`); expect(await getDateInputValue(page)).toEqual(`3/7/${year}`); @@ -1203,30 +1077,32 @@ describe("calcite-input-date-picker", () => { await page.waitForChanges(); const [startDatePicker, endDatePicker] = await page.findAll("calcite-input-date-picker >>> calcite-input-text"); + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + expect(await calendar.isVisible()).toBe(false); await startDatePicker.click(); await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); await navigateMonth(page, "previous"); - await selectDayInMonth(page, 1); - - await endDatePicker.click(); - await page.waitForChanges(); + await selectDayInMonthByIndex(page, 1); + expect(await calendar.isVisible()).toBe(true); await navigateMonth(page, "previous"); - await selectDayInMonth(page, 31); + await selectDayInMonthByIndex(page, 31); + expect(await calendar.isVisible()).toBe(false); inputDatePicker.setProperty("value", ["2022-10-01", "2022-10-31"]); await page.waitForChanges(); await startDatePicker.click(); await page.waitForChanges(); - + expect(await calendar.isVisible()).toBe(true); expect(await getActiveMonth(page)).toBe("October"); await endDatePicker.click(); await page.waitForChanges(); - + expect(await calendar.isVisible()).toBe(true); expect(await getActiveMonth(page)).toBe("October"); }); @@ -1266,15 +1142,22 @@ describe("calcite-input-date-picker", () => { it("should normalize year to current century when user types the value in range", async () => { const page = await newE2EPage(); await page.setContent(""); + const inputEl = await page.find("calcite-input-date-picker >>> calcite-input-text"); const element = await page.find("calcite-input-date-picker"); const changeEvent = await page.spyOnEvent("calciteInputDatePickerChange"); - await element.click(); + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + expect(await calendar.isVisible()).toBe(false); + + await inputEl.click(); await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + await page.keyboard.type("1/1/20"); await page.keyboard.press("Enter"); await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); expect(await element.getProperty("value")).toEqual(["2020-01-01", ""]); expect(changeEvent).toHaveReceivedEventTimes(1); @@ -1282,109 +1165,366 @@ describe("calcite-input-date-picker", () => { await page.keyboard.press("Enter"); await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); expect(await element.getProperty("value")).toEqual(["2020-01-01", "2020-02-02"]); expect(changeEvent).toHaveReceivedEventTimes(2); }); }); - describe("ArrowKeys and PageKeys", () => { - it("should be able to navigate between months using arrow keys and page keys", async () => { + describe("date-picker visibility in range", () => { + async function isCalendarVisible(page: E2EPage): Promise { + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + return await calendar.isVisible(); + } + + it("should keep date-picker open when user selects dates in range calendar", async () => { const page = await newE2EPage(); - await page.setContent(html``); - await page.waitForChanges(); + await page.setContent(html``); await skipAnimations(page); + await page.waitForChanges(); - const input = await page.find("calcite-input-date-picker >>> calcite-input-text"); - const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + const inputDatePicker = await page.find("calcite-input-date-picker"); + const startDatePicker = await page.find("calcite-input-date-picker >>> calcite-input-text"); + expect(await isCalendarVisible(page)).toBe(false); - expect(await calendar.isVisible()).toBe(false); - await input.click(); - expect(await calendar.isVisible()).toBe(true); + await startDatePicker.click(); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); - await page.keyboard.press("Tab"); + await selectDayInMonthByIndex(page, 1); await page.waitForChanges(); - await page.keyboard.press("Tab"); + expect(await isCalendarVisible(page)).toBe(true); + + await selectDayInMonthByIndex(page, 32); await page.waitForChanges(); - await page.keyboard.press("Tab"); + expect(await isCalendarVisible(page)).toBe(false); + expect(await inputDatePicker.getProperty("value")).not.toBeNull(); + }); + + it("should keep date-picker open when user select startDate from end calendar", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await skipAnimations(page); await page.waitForChanges(); - await page.keyboard.press("Tab"); + + const inputDatePicker = await page.find("calcite-input-date-picker"); + const startDatePicker = await page.find("calcite-input-date-picker >>> calcite-input-text"); + expect(await isCalendarVisible(page)).toBe(false); + + await startDatePicker.click(); await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); - await page.keyboard.press("ArrowUp"); + await selectDayInMonthByIndex(page, 35); await page.waitForChanges(); - expect(await calendar.isVisible()).toBe(true); + expect(await isCalendarVisible(page)).toBe(true); - await page.keyboard.press("PageUp"); + await selectDayInMonthByIndex(page, 52); await page.waitForChanges(); - expect(await calendar.isVisible()).toBe(true); + expect(await isCalendarVisible(page)).toBe(false); + expect(await inputDatePicker.getProperty("value")).not.toBeNull(); + }); - await page.keyboard.press("ArrowDown"); + it("should keep date-picker open when user select startDate from start calendar", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await skipAnimations(page); await page.waitForChanges(); - expect(await calendar.isVisible()).toBe(true); - await page.keyboard.press("PageDown"); + const inputDatePicker = await page.find("calcite-input-date-picker"); + const startDatePicker = await page.find("calcite-input-date-picker >>> calcite-input-text"); + expect(await isCalendarVisible(page)).toBe(false); + + await startDatePicker.click(); await page.waitForChanges(); - expect(await calendar.isVisible()).toBe(true); + expect(await isCalendarVisible(page)).toBe(true); - await page.keyboard.press("PageDown"); + await selectDayInMonthByIndex(page, 5); await page.waitForChanges(); - expect(await calendar.isVisible()).toBe(true); + expect(await isCalendarVisible(page)).toBe(true); - await page.keyboard.press("Enter"); + await selectDayInMonthByIndex(page, 22); await page.waitForChanges(); - expect(await calendar.isVisible()).toBe(false); - expect(await getActiveMonth(page)).toBe("February"); + expect(await isCalendarVisible(page)).toBe(false); + expect(await inputDatePicker.getProperty("value")).not.toBeNull(); }); - it("should be able to navigate between months using arrow keys and page keys in range", async () => { + it("should keep date-picker open when user is modifying the dates after initial selection", async () => { const page = await newE2EPage(); await page.setContent(html``); - await page.waitForChanges(); await skipAnimations(page); + await page.waitForChanges(); - await page.evaluate(() => { - const inputDatePicker = document.querySelector("calcite-input-date-picker"); - inputDatePicker.value = ["2024-01-01", "2024-02-10"]; - }); + const startDatePicker = await page.find("calcite-input-date-picker >>> calcite-input-text"); + expect(await isCalendarVisible(page)).toBe(false); - const inputDatePicker = await page.find("calcite-input-date-picker"); - const [inputStart, inputEnd] = await page.findAll("calcite-input-date-picker >>> calcite-input-text"); - const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); - expect(await calendar.isVisible()).toBe(false); + await startDatePicker.click(); + await page.waitForChanges(); - await inputStart.click(); - expect(await calendar.isVisible()).toBe(true); + await selectDayInMonthByIndex(page, 1); + await page.waitForChanges(); - await page.keyboard.press("Tab"); + await selectDayInMonthByIndex(page, 32); await page.waitForChanges(); + + await startDatePicker.click(); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + await navigateMonth(page, "previous", true); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + await navigateMonth(page, "previous", true); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + await selectDayInMonthByIndex(page, 1); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + await navigateMonth(page, "next", true); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + await selectDayInMonthByIndex(page, 32); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(false); + }); + + it("should be able to navigate months when valueAsDate is parsed", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await skipAnimations(page); + await page.waitForChanges(); + + const startDatePicker = await page.find("calcite-input-date-picker >>> calcite-input-text"); + + await page.$eval("calcite-input-date-picker", (element: any) => { + element.valueAsDate = [new Date("2024-05-25"), new Date("2024-06-25")]; + }); + + expect(await isCalendarVisible(page)).toBe(false); + + await startDatePicker.click(); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + await navigateMonth(page, "previous", true); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + await navigateMonth(page, "previous", true); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + await navigateMonth(page, "next", true); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + }); + + it("should set the endDate to empty and open the calendar when startDate is updated to date beyond initial endDate", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await skipAnimations(page); + await page.waitForChanges(); + + const inputDatePickerEl = await page.find("calcite-input-date-picker"); + const startDatePicker = await page.find("calcite-input-date-picker >>> calcite-input-text"); + inputDatePickerEl.setProperty("value", ["2024-05-25", "2024-06-20"]); + expect(await isCalendarVisible(page)).toBe(false); + + await startDatePicker.click(); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + await selectDayInMonthByIndex(page, 60); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + const value = await inputDatePickerEl.getProperty("value"); + expect(value).toEqual(["2024-06-29", ""]); + }); + + it("should be able to update dates using keyboard", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await skipAnimations(page); + await page.waitForChanges(); + + const inputDatePickerEl = await page.find("calcite-input-date-picker"); + const startDatePicker = await page.find("calcite-input-date-picker >>> calcite-input-text"); + + inputDatePickerEl.setProperty("value", ["2024-05-25", "2024-06-20"]); + expect(await isCalendarVisible(page)).toBe(false); + + await startDatePicker.click(); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + await page.keyboard.press("Tab"); await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + await page.keyboard.press("Enter"); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + await page.keyboard.press("Enter"); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + await navigateToDateInMonth(page, true, true); + + await page.keyboard.press("Enter"); + await page.waitForChanges(); + expect(await isCalendarVisible(page)).toBe(true); + + const value = await inputDatePickerEl.getProperty("value"); + expect(value).toEqual(["2024-03-01", "2024-06-20"]); + }); + }); + + describe("hover range", () => { + it("should add range-hover attribute for dates less than new startDate and greater than current startDate or greater than new endDate and less than current startDate", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + const datePicker = await page.find("calcite-input-date-picker"); + datePicker.setProperty("value", ["2024-01-10", "2024-02-10"]); + await page.waitForChanges(); + + const input = await page.find("calcite-input-date-picker >>> calcite-input-text"); + await input.click(); + await page.waitForChanges(); + + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + expect(await calendar.isVisible()).toBe(true); + + let dateInsideRange = await getDayById(page, "20240201"); + await dateInsideRange.hover(); + await page.waitForChanges(); + expect(await (await getDayById(page, "20240131")).getProperty("rangeHover")).toBe(true); + + dateInsideRange = await getDayById(page, "20240205"); + await dateInsideRange.hover(); + expect(await (await getDayById(page, "20240202")).getProperty("rangeHover")).toBe(true); + + dateInsideRange = await getDayById(page, "20240105"); + await dateInsideRange.hover(); + expect(await (await getDayById(page, "20240106")).getProperty("rangeHover")).toBe(true); + }); + + it("should add range-hover attribute for dates greater current endDate and less than new endDate or greater than new endDate or less than current endDate", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + const datePicker = await page.find("calcite-input-date-picker"); + datePicker.setProperty("value", ["2024-01-10", "2024-02-10"]); + await page.waitForChanges(); + await page.keyboard.press("Tab"); await page.waitForChanges(); await page.keyboard.press("Tab"); await page.waitForChanges(); + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + expect(await calendar.isVisible()).toBe(true); + + let dateInsideRange = await getDayById(page, "20240201"); + await dateInsideRange.hover(); + await page.waitForChanges(); + expect(await (await getDayById(page, "20240209")).getProperty("rangeHover")).toBe(true); + + dateInsideRange = await getDayById(page, "20240115"); + await dateInsideRange.hover(); + expect(await (await getDayById(page, "20240116")).getProperty("rangeHover")).toBe(true); + + let dateOutsideRange = await getDayById(page, "20240215"); + await dateOutsideRange.hover(); + expect(await (await getDayById(page, "20240212")).getProperty("rangeHover")).toBe(true); + + await navigateMonth(page, "next", true); + await page.waitForChanges(); + dateOutsideRange = await getDayById(page, "20240315"); + await dateOutsideRange.hover(); + expect(await (await getDayById(page, "20240314")).getProperty("rangeHover")).toBe(true); + }); + }); + + describe("ArrowKeys and PageKeys", () => { + it("should be able to navigate between months using arrow keys and page keys", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await page.waitForChanges(); + await skipAnimations(page); + + const input = await page.find("calcite-input-date-picker >>> calcite-input-text"); + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + + expect(await calendar.isVisible()).toBe(false); + await input.click(); + expect(await calendar.isVisible()).toBe(true); + + await navigateToDateInMonth(page, false); await page.keyboard.press("ArrowUp"); await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + await page.keyboard.press("PageUp"); await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + + await page.keyboard.press("PageDown"); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + + await page.keyboard.press("PageDown"); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + await page.keyboard.press("Enter"); await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(false); + expect(await getActiveMonth(page)).toBe("February"); + }); + + it("should be able to navigate between months using arrow keys and page keys in range", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await page.waitForChanges(); + await skipAnimations(page); + + await page.evaluate(() => { + const inputDatePicker = document.querySelector("calcite-input-date-picker"); + inputDatePicker.value = ["2024-01-01", "2024-02-10"]; + }); + const inputDatePicker = await page.find("calcite-input-date-picker"); + const input = await page.find("calcite-input-date-picker >>> calcite-input-text"); + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); expect(await calendar.isVisible()).toBe(false); - expect(await inputDatePicker.getProperty("value")).toEqual(["2023-11-25", "2024-02-10"]); - await inputEnd.click(); + await input.click(); expect(await calendar.isVisible()).toBe(true); - await page.keyboard.press("Tab"); - await page.waitForChanges(); - await page.keyboard.press("Tab"); + await navigateToDateInMonth(page); + + await page.keyboard.press("ArrowUp"); await page.waitForChanges(); - await page.keyboard.press("Tab"); + await page.keyboard.press("PageUp"); await page.waitForChanges(); - await page.keyboard.press("Tab"); + await page.keyboard.press("Enter"); await page.waitForChanges(); + + expect(await calendar.isVisible()).toBe(true); + expect(await inputDatePicker.getProperty("value")).toEqual(["2023-11-25", "2024-02-10"]); + await page.keyboard.press("ArrowDown"); await page.waitForChanges(); expect(await calendar.isVisible()).toBe(true); @@ -1399,4 +1539,324 @@ describe("calcite-input-date-picker", () => { expect(await inputDatePicker.getProperty("value")).not.toEqual(["2024-01-01", "2024-03-17"]); }); }); + + describe("last valid month", () => { + it("should not close date-picker when user navigate to last valid month", async () => { + const page = await newE2EPage(); + await page.setContent( + html``, + ); + + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + expect(await calendar.isVisible()).toBe(false); + + const input = await page.find("calcite-input-date-picker >>> calcite-input-text"); + await input.click(); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + + await navigateMonth(page, "next"); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + + await navigateMonth(page, "previous"); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + + await navigateMonth(page, "previous"); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + }); + + it("should not close date-picker when user navigate to last valid month in range", async () => { + const page = await newE2EPage(); + await page.setContent( + html``, + ); + + await page.$eval("calcite-input-date-picker", (element: HTMLCalciteInputDatePickerElement) => { + element.value = ["2024-08-15", "2024-09-15"]; + }); + + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + expect(await calendar.isVisible()).toBe(false); + + const input = await page.find("calcite-input-date-picker >>> calcite-input-text"); + await input.click(); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + + await navigateMonth(page, "next", true); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + + await navigateMonth(page, "previous", true); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + + await navigateMonth(page, "previous", true); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + }); + }); + + it("should update activeDate when user selects date from different month using keyboard", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await page.waitForChanges(); + await skipAnimations(page); + + const inputDatePicker = await page.find("calcite-input-date-picker"); + const input = await page.find("calcite-input-date-picker >>> calcite-input-text"); + inputDatePicker.setProperty("value", ["2025-09-21", "2025-11-11"]); + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + + expect(await calendar.isVisible()).toBe(false); + await input.click(); + expect(await calendar.isVisible()).toBe(true); + + await navigateToDateInMonth(page); + + await page.keyboard.press("ArrowRight"); + await page.waitForChanges(); + await page.keyboard.press("Enter"); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + expect(await getActiveMonth(page)).toBe("September"); + await page.keyboard.press("Escape"); + await page.waitForChanges(); + + expect(await calendar.isVisible()).toBe(false); + await input.click(); + expect(await calendar.isVisible()).toBe(true); + await navigateToDateInMonth(page); + + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + await page.keyboard.press("Enter"); + await page.waitForChanges(); + + expect(await calendar.isVisible()).toBe(true); + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + expect(await getActiveMonth(page)).toBe("September"); + + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + + expect(await getActiveMonth(page)).toBe("October"); + }); + + it("should not focus disabled dates when navigating using keyboard", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await page.waitForChanges(); + await skipAnimations(page); + + const inputDatePicker = await page.find("calcite-input-date-picker"); + const [startInput, endInput] = await page.findAll("calcite-input-date-picker >>> calcite-input-text"); + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + + expect(await calendar.isVisible()).toBe(false); + await startInput.click(); + expect(await calendar.isVisible()).toBe(true); + + await selectDayInMonthByIndex(page, 25); + expect(await calendar.isVisible()).toBe(true); + + await endInput.click(); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(false); + + await endInput.click(); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(true); + + await navigateToDateInMonth(page, true, true); + + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + await page.keyboard.press("ArrowDown"); + await page.waitForChanges(); + await page.keyboard.press("ArrowRight"); + await page.waitForChanges(); + await page.keyboard.press("ArrowRight"); + await page.waitForChanges(); + expect(await getDayById(page, "20240703")).not.toHaveAttribute("range-hover"); + + await page.keyboard.press("Enter"); + await page.waitForChanges(); + expect(await calendar.isVisible()).toBe(false); + const [, endDate] = await inputDatePicker.getProperty("value"); + expect(endDate).toEqual("2024-07-02"); + }); + + it("should not update endDate when startDate is updated", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await page.waitForChanges(); + await skipAnimations(page); + + const inputDatePicker = await page.find("calcite-input-date-picker"); + const startInput = await page.find("calcite-input-date-picker >>> calcite-input-text"); + + await page.$eval("calcite-input-date-picker", (element: HTMLCalciteInputDatePickerElement) => { + element.valueAsDate = [new Date("09-21-2025"), new Date("11-11-2025")]; + }); + + expect(await inputDatePicker.getProperty("value")).toEqual(["2025-09-21", "2025-11-11"]); + expect(await getDateInputValue(page, "end")).toBe("11/11/2025"); + expect(await getDateInputValue(page, "start")).toBe("9/21/2025"); + + await startInput.click(); + await page.waitForChanges(); + const newStartDate = await getDayById(page, "20250925"); + await newStartDate.click(); + await page.waitForChanges(); + + expect(await inputDatePicker.getProperty("value")).toEqual(["2025-09-25", "2025-11-11"]); + expect(await getDateInputValue(page, "end")).toBe("11/11/2025"); + expect(await getDateInputValue(page, "start")).toBe("9/25/2025"); + }); + + it("should not update startDate when endDate is updated", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + await page.waitForChanges(); + await skipAnimations(page); + + const inputDatePicker = await page.find("calcite-input-date-picker"); + const endInput = await page.find(`calcite-input-date-picker >>> div[data-position="end"] >>> calcite-input-text`); + await page.$eval("calcite-input-date-picker", (element: HTMLCalciteInputDatePickerElement) => { + element.valueAsDate = [new Date("09-21-2025"), new Date("11-11-2025")]; + }); + + await endInput.click(); + await page.waitForChanges(); + const newEndDate = await getDayById(page, "20251005"); + await newEndDate.click(); + await page.waitForChanges(); + + expect(await inputDatePicker.getProperty("value")).toEqual(["2025-09-21", "2025-10-05"]); + expect(await getDateInputValue(page, "end")).toBe("10/5/2025"); + expect(await getDateInputValue(page, "start")).toBe("9/21/2025"); + }); + + it("should not shift focus back on input-date-picker when other input elements are clicked", async () => { + const page = await newE2EPage(); + await page.setContent( + html` + `, + ); + + const input = await page.find("calcite-input"); + const inputDatePicker = await page.find("calcite-input-date-picker"); + const calendar = await page.find(`calcite-input-date-picker >>> .${CSS.calendarWrapper}`); + expect(await calendar.isVisible()).toBe(false); + + await inputDatePicker.click(); + expect(await calendar.isVisible()).toBe(true); + expect(await isElementFocused(page, "#input-date")).toBe(true); + + await input.click(); + expect(await calendar.isVisible()).toBe(false); + expect(await isElementFocused(page, "#input")).toBe(true); + }); }); + +async function selectDayInMonthByIndex(page: E2EPage, day: number): Promise { + const dayIndex = day - 1; + const days = await page.findAll("calcite-input-date-picker >>> calcite-date-picker-day[current-month]"); + await days[dayIndex].click(); + await page.waitForChanges(); +} + +async function getActiveMonth(page: E2EPage, position: Extract<"start" | "end", Position> = "start"): Promise { + const [startMonth, endMonth] = await page.findAll( + `calcite-input-date-picker >>> calcite-date-picker-month-header >>> .${MONTH_HEADER_CSS.header} >>> calcite-select.${MONTH_HEADER_CSS.monthPicker}`, + ); + + const selectedMonth = + position === "start" + ? await startMonth.find("calcite-option[selected]") + : await endMonth.find("calcite-option[selected]"); + return selectedMonth.textContent; +} + +async function getDateInputValue(page: E2EPage, type: "start" | "end" = "start"): Promise { + const inputIndex = type === "start" ? 0 : 1; + + return page.evaluate( + async (inputIndex: number): Promise => + document + .querySelector("calcite-input-date-picker") + .shadowRoot.querySelectorAll("calcite-input-text") + [inputIndex].shadowRoot.querySelector("input").value, + inputIndex, + ); +} + +async function navigateMonth(page: E2EPage, direction: "previous" | "next", range = false): Promise { + const [datePickerMonthHeaderStart, datePickerMonthHeaderEnd] = await page.findAll( + `calcite-input-date-picker >>> calcite-date-picker-month-header >>> .${MONTH_HEADER_CSS.header}`, + ); + + let prevMonth: E2EElement; + let nextMonth: E2EElement; + if (range) { + prevMonth = await datePickerMonthHeaderStart.find("calcite-action"); + nextMonth = await datePickerMonthHeaderEnd.find("calcite-action"); + } else { + [prevMonth, nextMonth] = await datePickerMonthHeaderStart.findAll("calcite-action"); + } + + await (direction === "previous" ? prevMonth.click() : nextMonth.click()); + await page.waitForChanges(); +} + +async function getDayById(page: E2EPage, id: string): Promise { + return await page.find( + `calcite-input-date-picker >>> calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day[current-month][id="${id}"]`, + ); +} + +async function navigateToDateInMonth( + page: E2EPage, + isRange = true, + isPreviousMonthChevronDisabled = false, +): Promise { + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + if (!isPreviousMonthChevronDisabled) { + await page.keyboard.press("Tab"); + await page.waitForChanges(); + } + if (!isRange) { + await page.keyboard.press("Tab"); + await page.waitForChanges(); + } +} diff --git a/packages/calcite-components/src/components/input-date-picker/input-date-picker.scss b/packages/calcite-components/src/components/input-date-picker/input-date-picker.scss index 68629ade246..fddda1133a5 100644 --- a/packages/calcite-components/src/components/input-date-picker/input-date-picker.scss +++ b/packages/calcite-components/src/components/input-date-picker/input-date-picker.scss @@ -48,6 +48,9 @@ .chevron-icon { color: var(--calcite-color-text-1); } + ~ .input-wrapper .chevron-icon { + color: var(--calcite-color-text-1); + } } } @@ -59,25 +62,43 @@ } :host([range]) { - .input-container { + .container { @apply flex; } + .input-container { + @apply flex flex-auto; + } + .input-wrapper { @apply flex-auto; } +} - .horizontal-arrow-container { - @apply bg-background - border-color-input - flex - items-center - border - border-l-0 - border-r-0 - border-solid - py-0 - px-1; +.divider-container { + @apply flex items-stretch border-color-input + border + border-l-0 + border-r-0 + border-solid; + background-color: var(--calcite-color-foreground-1); +} + +:host([layout="horizontal"]) .divider-container { + @apply w-px; +} + +.divider { + @apply inline-block w-px; + margin-block: var(--calcite-size-xxs); + background-color: var(--calcite-color-border-2); +} + +:host([layout="vertical"]) .divider-container { + @apply w-full h-px border-t-0 border-b-0 border-l border-r-0; + padding-inline: var(--calcite-size-md); + & .divider { + @apply w-full h-px my-0; } } @@ -87,22 +108,7 @@ } .input-container { - @apply flex-col - items-start; - } - - .calendar-wrapper--end { - transform: translate3d(0, 0, 0); - } - - .vertical-arrow-container { - inset-block-start: theme("spacing.6"); - @apply bg-foreground-1 - absolute - z-default - mx-px - px-2.5; - inset-inline-start: 0; + @apply flex-col items-start; } } @@ -127,25 +133,43 @@ @apply mt-0; } -:host([range][layout="vertical"][scale="m"]) .vertical-arrow-container { - inset-block-start: theme("spacing.6"); - padding-inline-start: theme("spacing.3"); +.vertical-chevron-container { + @apply flex items-center border border-solid border-color-input border-l-0; + padding-inline: var(--calcite-size-md); + background-color: var(--calcite-color-foreground-1); calcite-icon { - @apply h-3 - w-3 - min-w-0; + color: var(--calcite-color-text-3); + + &:hover { + color: var(--calcite-color-text-1); + } + } +} + +:host([range][layout="vertical"][scale="s"]) { + .vertical-chevron-container, + .divider-container { + padding-inline: var(--calcite-size-sm); } } -:host([range][layout="vertical"][scale="l"]) .vertical-arrow-container { - inset-block-start: theme("spacing.9"); - @apply px-3.5; +:host([range][layout="vertical"][scale="l"]) { + .vertical-chevron-container, + .divider-container { + padding-inline: var(--calcite-size-lg); + } } -:host([range][layout="vertical"][open]) { - .vertical-arrow-container { - @apply hidden; +.container { + &:focus-within, + &:active, + &:hover { + .vertical-chevron-container { + calcite-icon { + color: var(--calcite-color-text-1); + } + } } } diff --git a/packages/calcite-components/src/components/input-date-picker/input-date-picker.stories.ts b/packages/calcite-components/src/components/input-date-picker/input-date-picker.stories.ts index d88f117b2e9..891bfc0557f 100644 --- a/packages/calcite-components/src/components/input-date-picker/input-date-picker.stories.ts +++ b/packages/calcite-components/src/components/input-date-picker/input-date-picker.stories.ts @@ -1,4 +1,4 @@ -import { createBreakpointStories, modesDarkDefault } from "../../../.storybook/utils"; +import { boolean, createBreakpointStories, modesDarkDefault } from "../../../.storybook/utils"; import { html } from "../../../support/formatting"; import { locales, defaultLocale } from "../../utils/locale"; import { defaultMenuPlacement, menuPlacements } from "../../utils/floating-ui"; @@ -53,23 +53,47 @@ export default { }; export const simple = (args: InputDatePickerStoryArgs): string => html` -
+ +
`; -export const range = (): string => html` -
+export const withMinMax = (): string => + html` +
+ +
`; + +export const rangeWithMinMax = (): string => html` + +
html` prev-month-label="Previous month" range layout="horizontal" + open >
`; @@ -106,8 +131,14 @@ export const flipPlacements_TestOnly = (): string => html` `; -export const mediumIconForLargeInput_TestOnly = (): string => html` -
+export const chineseLang_TestOnly = (): string => html` + +
html` `; export const readOnlyHasNoDropdownAffordance_TestOnly = (): string => html` -
- -
+ `; export const validationMessageAllScales_TestOnly = (): string => html` @@ -216,15 +245,22 @@ export const scales_TestOnly = (): string => html` `; export const arabicLocaleDarkModeRTL_TestOnly = (): string => html` -
+ +
`; arabicLocaleDarkModeRTL_TestOnly.parameters = { themes: modesDarkDefault }; @@ -234,6 +270,62 @@ export const widthSetToBreakpoints_TestOnly = (): string => html``, ); +export const rangeWithValueAsDate = (): string => html` + +
+ +
+ +`; + +export const rangeWithValue = (): string => html` + +
+ +
+ +`; + +export const rangeWithMinAfterCurrentDate = (): string => html` + +
+ +
+`; + +export const rangeWithMaxBeforeCurrentDate = (): string => html` + +
+ +
+`; + export const Focus = (): string => html` `; -Focus.parameters = { - chromatic: { delay: 2000 }, -}; - export const localeFormatting = (): string => html` -
- - -
+ + `; diff --git a/packages/calcite-components/src/components/input-date-picker/input-date-picker.tsx b/packages/calcite-components/src/components/input-date-picker/input-date-picker.tsx index 101a129d177..fbabd1f6a1c 100644 --- a/packages/calcite-components/src/components/input-date-picker/input-date-picker.tsx +++ b/packages/calcite-components/src/components/input-date-picker/input-date-picker.tsx @@ -5,7 +5,6 @@ import { EventEmitter, h, Host, - Listen, Method, Prop, State, @@ -186,12 +185,12 @@ export class InputDatePicker } @Watch("valueAsDate") - valueAsDateWatcher(valueAsDate: Date): void { - this.datePickerActiveDate = valueAsDate; - const newValue = - this.range && Array.isArray(valueAsDate) - ? [dateToISO(valueAsDate[0]), dateToISO(valueAsDate[1])] - : dateToISO(valueAsDate); + valueAsDateWatcher(valueAsDate: Date | Date[]): void { + const newValue = Array.isArray(valueAsDate) + ? [dateToISO(valueAsDate[0]), dateToISO(valueAsDate[1])] + : dateToISO(valueAsDate); + this.datePickerActiveDate = Array.isArray(valueAsDate) ? valueAsDate[0] : valueAsDate; + if (this.value !== newValue) { this.valueAsDateChangedExternally = true; this.value = newValue; @@ -259,6 +258,11 @@ export class InputDatePicker this.maxAsDate = dateFromISO(max); } + /** + * Specifies the monthStyle used by the component. + */ + @Prop() monthStyle: "abbreviated" | "wide" = "wide"; + /** When `true`, displays the `calcite-date-picker` component. */ @Prop({ mutable: true, reflect: true }) open = false; @@ -363,15 +367,6 @@ export class InputDatePicker // //-------------------------------------------------------------------------- - @Listen("calciteDaySelect") - calciteDaySelectHandler(): void { - if (this.shouldFocusRangeStart() || this.shouldFocusRangeEnd()) { - return; - } - - this.open = false; - } - private calciteInternalInputInputHandler = (event: CustomEvent): void => { const target = event.target as HTMLCalciteInputElement; const value = target.value; @@ -469,7 +464,6 @@ export class InputDatePicker connectedCallback(): void { connectLocalized(this); - this.handleDateTimeFormatChange(); const { open } = this; open && this.openHandler(); @@ -558,135 +552,139 @@ export class InputDatePicker {this.localeData && ( -
-
- - {!this.readOnly && - this.renderToggleIcon(this.open && this.focusedInput === "start")} - -
- - - {this.range && this.layout === "horizontal" && ( -
- -
- )} - {this.range && this.layout === "vertical" && this.scale !== "s" && ( -
- -
- )} - {this.range && ( +
+
{!this.readOnly && - this.renderToggleIcon(this.open && this.focusedInput === "end")} + !this.range && + this.renderToggleIcon(this.open && this.focusedInput === "start")} + +
+ + {this.range && ( +
+
+
+ )} + {this.range && ( +
+ + {!this.readOnly && + this.layout === "horizontal" && + this.renderToggleIcon(this.open)} +
+ )} +
+ {this.range && this.layout === "vertical" && ( +
+
)}
@@ -792,12 +790,13 @@ export class InputDatePicker private userChangedValue = false; + private rangeStartValueChangedByUser = false; + openTransitionProp = "opacity"; transitionEl: HTMLDivElement; @Watch("layout") - @Watch("focusedInput") setReferenceEl(): void { const { focusedInput, layout, endWrapper, startWrapper } = this; @@ -879,7 +878,6 @@ export class InputDatePicker onClose(): void { this.calciteInputDatePickerClose.emit(); deactivateFocusTrap(this); - this.restoreInputFocus(); this.focusOnOpen = false; this.datePickerEl.reset(); } @@ -950,6 +948,10 @@ export class InputDatePicker return; } + const targeHasSelect = event + .composedPath() + .some((el: HTMLElement) => el.tagName === "CALCITE-SELECT"); + if (key === "Enter") { event.preventDefault(); this.commitValue(); @@ -961,12 +963,16 @@ export class InputDatePicker } if (submitForm(this)) { - this.restoreInputFocus(); + this.restoreInputFocus(true); } - } else if (key === "ArrowDown") { + } else if ((key === "ArrowDown" || key === "ArrowUp") && !targeHasSelect) { this.open = true; this.focusOnOpen = true; event.preventDefault(); + } else if (key === "Escape") { + this.open = false; + event.preventDefault(); + this.restoreInputFocus(true); } }; @@ -1064,14 +1070,26 @@ export class InputDatePicker this.restoreInputFocus(); }; - private restoreInputFocus(): void { + private restoreInputFocus(isDatePickerClosed = false): void { if (!this.range) { this.startInput.setFocus(); + this.open = false; return; } - const focusedInput = this.focusedInput === "start" ? this.startInput : this.endInput; - focusedInput.setFocus(); + if (isDatePickerClosed) { + this.focusInput(); + return; + } + + this.rangeStartValueChangedByUser = this.focusedInput === "start"; + this.focusedInput = "end"; + + if (this.shouldFocusRangeStart() || this.rangeStartValueChangedByUser) { + return; + } + this.open = false; + this.focusInput(); } private localizeInputValues(): void { @@ -1217,4 +1235,9 @@ export class InputDatePicker const normalizedYear = normalizeToCurrentCentury(Number(year)); return `${normalizedYear}-${month}-${day}`; } + + private focusInput = (): void => { + const focusedInput = this.focusedInput === "start" ? this.startInput : this.endInput; + focusedInput.setFocus(); + }; } diff --git a/packages/calcite-components/src/components/input-date-picker/resources.ts b/packages/calcite-components/src/components/input-date-picker/resources.ts index 19a65e77795..d06ed9c58df 100644 --- a/packages/calcite-components/src/components/input-date-picker/resources.ts +++ b/packages/calcite-components/src/components/input-date-picker/resources.ts @@ -1,17 +1,22 @@ export const CSS = { assistiveText: "assistive-text", calendarWrapper: "calendar-wrapper", - calendarWrapperEnd: "calendar-wrapper--end", + container: "container", + dividerContainer: "divider-container", + divider: "divider", horizontalArrowContainer: "horizontal-arrow-container", inputBorderTopColorOne: "border-top-color-one", inputContainer: "input-container", - inputNoBottomBorder: "no-bottom-border", + inputNoBottomBorder: "input--no-bottom-border", + inputNoRightBorder: "input--no-right-border", + inputNoTopBorder: "input--no-top-border", + inputNoLeftBorder: "input--no-left-border", inputWrapper: "input-wrapper", input: "input", menu: "menu-container", menuActive: "menu-container--active", toggleIcon: "toggle-icon", - verticalArrowContainer: "vertical-arrow-container", + verticalChevronContainer: "vertical-chevron-container", chevronIcon: "chevron-icon", }; diff --git a/packages/calcite-components/src/components/input-text/input-text.scss b/packages/calcite-components/src/components/input-text/input-text.scss index b1bdab1fc5e..e5ad6e46e1a 100755 --- a/packages/calcite-components/src/components/input-text/input-text.scss +++ b/packages/calcite-components/src/components/input-text/input-text.scss @@ -329,10 +329,22 @@ input[type="text"]::-ms-reveal { items-center; } -:host(.no-bottom-border) input { +:host(.input--no-bottom-border) input { @apply border-b-0; } +:host(.input--no-top-border) input { + @apply border-t-0; +} + +:host(.input--no-right-border) input { + border-inline-end: 0; +} + +:host(.input--no-left-border) input { + border-inline-start: 0; +} + :host(.border-top-color-one) input { @apply border-t-color-1; } diff --git a/packages/calcite-components/src/components/select/select.scss b/packages/calcite-components/src/components/select/select.scss index ba951fb35eb..e77d5cf925a 100644 --- a/packages/calcite-components/src/components/select/select.scss +++ b/packages/calcite-components/src/components/select/select.scss @@ -5,17 +5,20 @@ * * @prop --calcite-select-font-size: The font size of `calcite-option`s in the component. * @prop --calcite-select-spacing: The padding around the selected option text. + * @prop --calcite-select-text-color: The text color of the component. */ :host { @extend %component-spacing; @apply flex flex-col; + font-size: var(--calcite-select-font-size); + font-weight: var(--calcite-internal-select-font-weight, var(--calcite-font-weight-regular)); } .wrapper { @apply relative flex items-stretch; inline-size: var(--select-width); - + block-size: var(--calcite-internal-select-block-size, #{$calcite-size-32}); &:focus-within, &:active, &:hover { @@ -30,40 +33,21 @@ :host([scale="s"]) { --calcite-select-font-size: theme("fontSize.n2h"); --calcite-select-spacing-inline: theme("spacing.2") theme("spacing.8"); - - .wrapper { - @apply h-6; - } - - .icon-container { - @apply px-2; - } + --calcite-internal-select-icon-container-padding-inline: var(--calcite-size-sm); + --calcite-internal-select-block-size: #{$calcite-size-24}; } :host([scale="m"]) { --calcite-select-font-size: theme("fontSize.n1h"); --calcite-select-spacing-inline: theme("spacing.3") theme("spacing.10"); - - .wrapper { - @apply h-8; - } - - .icon-container { - @apply px-3; - } + --calcite-internal-select-icon-container-padding-inline: var(--calcite-size-md); } :host([scale="l"]) { --calcite-select-font-size: theme("fontSize.0h"); --calcite-select-spacing-inline: theme("spacing.4") theme("spacing.12"); - - .wrapper { - block-size: 44px; - } - - .icon-container { - @apply px-4; - } + --calcite-internal-select-icon-container-padding-inline: var(--calcite-size-lg); + --calcite-internal-select-block-size: #{$calcite-size-44}; } :host([width="auto"]) { @@ -79,11 +63,27 @@ } .select { - @apply bg-foreground-1 border-color-input text-color-2 font-inherit focus-base m-0 box-border w-full cursor-pointer appearance-none truncate rounded-none border border-solid; - font-size: var(--calcite-select-font-size); + @apply bg-foreground-1 + border-color-input + text-color-2 + font-inherit + focus-base + m-0 + box-border + w-full + cursor-pointer + appearance-none + truncate + rounded-none + border-solid; + font-size: inherit; + font-weight: inherit; + color: var(--calcite-select-text-color, var(--calcite-color-text-2)); + border-width: var(--calcite-select-internal-border-width, var(--calcite-border-width-sm)); padding-inline: var(--calcite-select-spacing-inline); + padding-block: var(--calcite-internal-select-spacing-block); border-inline-end-width: theme("borderWidth.0"); - + line-height: var(--calcite-select-line-height, normal); &:focus { @apply focus-inset; } @@ -97,8 +97,9 @@ select:disabled { .icon-container { @apply border-color-input text-color-2 pointer-events-none absolute inset-y-0 flex items-center border-0 border-solid bg-transparent; inset-inline-end: theme("inset.0"); - border-inline-width: theme("borderWidth.0") theme("borderWidth.DEFAULT"); - + border-inline-width: theme("borderWidth.0") + var(--calcite-select-internal-icon-border-inline-end-width, theme("borderWidth.DEFAULT")); + padding-inline: var(--calcite-internal-select-icon-container-padding-inline); .icon { color: var(--calcite-color-text-3); } diff --git a/packages/calcite-components/src/utils/date.spec.ts b/packages/calcite-components/src/utils/date.spec.ts index f651fd3c8b4..7665ef83544 100644 --- a/packages/calcite-components/src/utils/date.spec.ts +++ b/packages/calcite-components/src/utils/date.spec.ts @@ -4,12 +4,15 @@ import english from "../components/date-picker/assets/date-picker/nls/en.json"; import french from "../components/date-picker/assets/date-picker/nls/fr.json"; import korean from "../components/date-picker/assets/date-picker/nls/ko.json"; import { + getDateInMonth, dateFromISO, dateFromRange, datePartsFromISO, dateToISO, formatCalendarYear, + getFirstValidDateInMonth, getOrder, + hasSameMonthAndYear, inRange, nextMonth, parseCalendarYear, @@ -257,3 +260,27 @@ describe("datePartsFromISO", () => { expect(datePartsFromISO("00-08-01")).toEqual({ day: "01", month: "08", year: "00" }); }); }); + +describe("getDateInMonth", () => { + it("return date in specified month", () => { + expect(getDateInMonth(new Date(2020, 0, 1), 4)).toEqual(new Date(2020, 4, 1)); + expect(getDateInMonth(new Date(2020, 0, 1), 12)).toEqual(new Date(2021, 0, 1)); + }); +}); + +describe("getFirstValidDateInMonth", () => { + it("return first valid date in month", () => { + const min = new Date(2020, 0, 1); + const max = new Date(2020, 11, 31); + expect(getFirstValidDateInMonth(new Date(2020, 4, 1), min, max)).toEqual(new Date(2020, 4, 1)); + expect(getFirstValidDateInMonth(new Date(2021, 0, 1), min, max)).toEqual(new Date(2020, 11, 31)); + }); +}); + +describe("hasSameMonthAndYear", () => { + it("return true if two dates have same month & year", () => { + expect(hasSameMonthAndYear(new Date(2020, 4, 1), new Date(2020, 4, 22))).toEqual(true); + expect(hasSameMonthAndYear(new Date(2020, 0, 1), new Date(2019, 12, 1))).toEqual(true); + expect(hasSameMonthAndYear(new Date(2020, 1, 1), new Date(2020, 2, 1))).toEqual(false); + }); +}); diff --git a/packages/calcite-components/src/utils/date.ts b/packages/calcite-components/src/utils/date.ts index 9e5deb70d17..2cf231d670a 100644 --- a/packages/calcite-components/src/utils/date.ts +++ b/packages/calcite-components/src/utils/date.ts @@ -202,6 +202,31 @@ export function prevMonth(date: Date): Date { return nextDate; } +/** + * Get active date in a given month. + * + * @param date + * @param month + */ +export function getDateInMonth(date: Date, month: number): Date { + const nextDate = new Date(date); + nextDate.setMonth(month); + return nextDate; +} + +/** + * Get First Valid date in a month. + * + * @param date + * @param min + * @param max + */ +export function getFirstValidDateInMonth(date: Date, min: Date, max: Date): Date { + const newDate = new Date(date); + newDate.setDate(1); + return inRange(newDate, min, max) ? newDate : dateFromRange(newDate, min, max); +} + /** * Get a date one month in the future * @@ -273,3 +298,15 @@ export function setEndOfDay(date: Date): Date { date.setHours(23, 59, 59, 999); return date; } + +/** + * + * Returns true if two dates have same month and year. + * + * @param date1 + * @param date2 + * @returns {boolean} + */ +export function hasSameMonthAndYear(date1: Date, date2: Date): boolean { + return date1 && date2 && date1.getMonth() === date2.getMonth() && date1.getFullYear() === date2.getFullYear(); +}