From 549d46c502a59acfc6b5baaf283e82bec0012888 Mon Sep 17 00:00:00 2001 From: davidemarcoli Date: Tue, 16 Apr 2024 10:08:52 +0200 Subject: [PATCH 01/12] Add new component six-date Start to move six-date from popup to popover --- libraries/ui-library/src/components.d.ts | 304 ++++++- .../six-date/components/day-selection.tsx | 42 + .../six-date/components/month-selection.tsx | 38 + .../six-date/components/year-selection.tsx | 32 + .../src/components/six-date/index.html | 564 ++++++++++++ .../src/components/six-date/readme.md | 128 +++ .../components/six-date/six-date-formats.tsx | 24 + .../src/components/six-date/six-date.scss | 178 ++++ .../src/components/six-date/six-date.tsx | 821 ++++++++++++++++++ .../six-date/test/six-datepicker.spec.tsx | 26 + .../test/six-datepicker.test-helpers.ts | 16 + 11 files changed, 2164 insertions(+), 9 deletions(-) create mode 100644 libraries/ui-library/src/components/six-date/components/day-selection.tsx create mode 100644 libraries/ui-library/src/components/six-date/components/month-selection.tsx create mode 100644 libraries/ui-library/src/components/six-date/components/year-selection.tsx create mode 100644 libraries/ui-library/src/components/six-date/index.html create mode 100644 libraries/ui-library/src/components/six-date/readme.md create mode 100644 libraries/ui-library/src/components/six-date/six-date-formats.tsx create mode 100644 libraries/ui-library/src/components/six-date/six-date.scss create mode 100644 libraries/ui-library/src/components/six-date/six-date.tsx create mode 100644 libraries/ui-library/src/components/six-date/test/six-datepicker.spec.tsx create mode 100644 libraries/ui-library/src/components/six-date/test/six-datepicker.test-helpers.ts diff --git a/libraries/ui-library/src/components.d.ts b/libraries/ui-library/src/components.d.ts index 595f1308f..6e9cba237 100644 --- a/libraries/ui-library/src/components.d.ts +++ b/libraries/ui-library/src/components.d.ts @@ -7,8 +7,10 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { AlertType } from "./components/six-alert/six-alert"; import { EmptyPayload } from "./utils/types"; -import { SixDateFormats } from "./components/six-datepicker/six-date-formats"; -import { SixDatepickerSelectPayload } from "./components/six-datepicker/six-datepicker"; +import { SixDateFormats } from "./components/six-date/six-date-formats"; +import { SixDatepickerSelectPayload } from "./components/six-date/six-date"; +import { SixDateFormats as SixDateFormats1 } from "./components/six-datepicker/six-date-formats"; +import { SixDatepickerSelectPayload as SixDatepickerSelectPayload1 } from "./components/six-datepicker/six-datepicker"; import { SixMenuItemData } from "./components/six-menu/six-menu"; import { SixDropdownAsyncFilterPayload, SixDropdownAutoFilterPayload, SixDropdownScrollPayload } from "./components/six-dropdown/six-dropdown"; import { SixFileListDownloadPayload, SixFileListRemovePayload } from "./components/six-file-list-item/six-file-list-item"; @@ -27,8 +29,10 @@ import { TimeFormat } from "./utils/time.util"; import { SixTimepickerChange } from "./components/six-timepicker/six-timepicker"; export { AlertType } from "./components/six-alert/six-alert"; export { EmptyPayload } from "./utils/types"; -export { SixDateFormats } from "./components/six-datepicker/six-date-formats"; -export { SixDatepickerSelectPayload } from "./components/six-datepicker/six-datepicker"; +export { SixDateFormats } from "./components/six-date/six-date-formats"; +export { SixDatepickerSelectPayload } from "./components/six-date/six-date"; +export { SixDateFormats as SixDateFormats1 } from "./components/six-datepicker/six-date-formats"; +export { SixDatepickerSelectPayload as SixDatepickerSelectPayload1 } from "./components/six-datepicker/six-datepicker"; export { SixMenuItemData } from "./components/six-menu/six-menu"; export { SixDropdownAsyncFilterPayload, SixDropdownAutoFilterPayload, SixDropdownScrollPayload } from "./components/six-dropdown/six-dropdown"; export { SixFileListDownloadPayload, SixFileListRemovePayload } from "./components/six-file-list-item/six-file-list-item"; @@ -256,6 +260,128 @@ export namespace Components { */ "value": string; } + /** + * @since 1.0 TODO: replace with correct version + * @status stable + */ + interface SixDate { + /** + * Callback to determine which date in the datepicker should be selectable. the callback function will get a datestring as an argument, e.g. '2021-07-04' Usage e.g.: const datepicker = document.getElementById('allowed-date-picker'); datepicker.allowedDates = datestring => parseInt(datestring.split('-')[2], 10) % 2 === 0; + */ + "allowedDates": (date: Date) => boolean; + /** + * Set to true to add a clear button when the input is populated. + */ + "clearable": boolean; + /** + * Closes the datepicker dropdown after selection + */ + "closeOnSelect": boolean; + /** + * The dropdown will close when the user interacts outside of this element (e.g. clicking). + */ + "containingElement"?: HTMLElement; + /** + * Define the dateFormat. Valid formats are: 'dd.mm.yyyy' 'yyyy-mm-dd' 'dd-mm-yyyy' 'dd/mm/yyyy' 'yyyy/mm/dd' 'dd.mm.yy' 'yy-mm-dd' 'dd-mm-yy' 'dd/mm/yy' 'yy/mm/dd' + */ + "dateFormat": SixDateFormats; + /** + * Set the amount of time, in milliseconds, to wait to trigger the `dateChange` event after each keystroke. + */ + "debounce": number; + /** + * The date to defines where the datepicker popup starts. The prop accepts ISO 8601 date strings (YYYY-MM-DD). + */ + "defaultDate"?: string; + /** + * If `true` the component is disabled. + */ + "disabled": boolean; + /** + * The error message shown, if `invalid` is set to true. + */ + "errorText": string | string[]; + /** + * The number of error texts to be shown (if the error-text slot isn't used). Defaults to 1 + */ + "errorTextCount"?: number; + /** + * Enable this option to prevent the panel from being clipped when the component is placed inside a container with `overflow: auto|scroll`. + */ + "hoist": boolean; + /** + * Set the position of the icon + */ + "iconPosition": 'left' | 'right'; + /** + * Indicates whether or not the calendar should be shown as an inline (always open) component + */ + "inline": boolean; + /** + * If this property is set to true and an error message is provided by `errorText`, the error message is displayed. + */ + "invalid": boolean; + /** + * The label text. + */ + "label": string; + /** + * The language used to render the weekdays and months. + */ + "locale": 'en' | 'de' | 'fr' | 'it' | 'es'; + /** + * The maximum datetime allowed. Value must be a date object + */ + "max"?: Date; + /** + * The minimum datetime allowed. Value must be a date object + */ + "min"?: Date; + /** + * The input's name attribute. + */ + "name": string; + /** + * Indicates whether or not the calendar dropdown is open on startup. You can use this in lieu of the show/hide methods. + */ + "open": boolean; + /** + * The placeholder defines what text to be shown on the input element + */ + "placeholder"?: string; + /** + * The enforced placement of the dropdown panel. + */ + "placement"?: 'top' | 'bottom'; + /** + * If `true` the user can only select a date via the component in the popup, but not directly edit the input field. + */ + "readonly": boolean; + /** + * Set to true to show an asterisk beneath the label. + */ + "required": boolean; + /** + * Selects an option + */ + "select": (datestring?: string) => Promise; + /** + * Sets focus on the datepickers input. + */ + "setFocus": (options?: FocusOptions) => Promise; + /** + * Datepicker size. + */ + "size": 'small' | 'medium' | 'large'; + /** + * Set the type. + */ + "type": 'date' | 'date-time'; + /** + * The value of the form field, which accepts a date object. + */ + "value"?: Date; + } /** * @since 1.0 * @status stable @@ -280,7 +406,7 @@ export namespace Components { /** * Define the dateFormat. Valid formats are: 'dd.mm.yyyy' 'yyyy-mm-dd' 'dd-mm-yyyy' 'dd/mm/yyyy' 'yyyy/mm/dd' 'dd.mm.yy' 'yy-mm-dd' 'dd-mm-yy' 'dd/mm/yy' 'yy/mm/dd' */ - "dateFormat": SixDateFormats; + "dateFormat": SixDateFormats1; /** * Set the amount of time, in milliseconds, to wait to trigger the `dateChange` event after each keystroke. */ @@ -2018,6 +2144,10 @@ export interface SixCheckboxCustomEvent extends CustomEvent { detail: T; target: HTMLSixCheckboxElement; } +export interface SixDateCustomEvent extends CustomEvent { + detail: T; + target: HTMLSixDateElement; +} export interface SixDatepickerCustomEvent extends CustomEvent { detail: T; target: HTMLSixDatepickerElement; @@ -2223,11 +2353,34 @@ declare global { prototype: HTMLSixCheckboxElement; new (): HTMLSixCheckboxElement; }; - interface HTMLSixDatepickerElementEventMap { + interface HTMLSixDateElementEventMap { "six-datepicker-select": SixDatepickerSelectPayload; "six-datepicker-clear": EmptyPayload; "six-datepicker-blur": SixDatepickerSelectPayload; } + /** + * @since 1.0 TODO: replace with correct version + * @status stable + */ + interface HTMLSixDateElement extends Components.SixDate, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLSixDateElement, ev: SixDateCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLSixDateElement, ev: SixDateCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLSixDateElement: { + prototype: HTMLSixDateElement; + new (): HTMLSixDateElement; + }; + interface HTMLSixDatepickerElementEventMap { + "six-datepicker-select": SixDatepickerSelectPayload1; + "six-datepicker-clear": EmptyPayload; + "six-datepicker-blur": SixDatepickerSelectPayload1; + } /** * @since 1.0 * @status stable @@ -3031,6 +3184,7 @@ declare global { "six-button": HTMLSixButtonElement; "six-card": HTMLSixCardElement; "six-checkbox": HTMLSixCheckboxElement; + "six-date": HTMLSixDateElement; "six-datepicker": HTMLSixDatepickerElement; "six-details": HTMLSixDetailsElement; "six-dialog": HTMLSixDialogElement; @@ -3297,6 +3451,132 @@ declare namespace LocalJSX { */ "value"?: string; } + /** + * @since 1.0 TODO: replace with correct version + * @status stable + */ + interface SixDate { + /** + * Callback to determine which date in the datepicker should be selectable. the callback function will get a datestring as an argument, e.g. '2021-07-04' Usage e.g.: const datepicker = document.getElementById('allowed-date-picker'); datepicker.allowedDates = datestring => parseInt(datestring.split('-')[2], 10) % 2 === 0; + */ + "allowedDates"?: (date: Date) => boolean; + /** + * Set to true to add a clear button when the input is populated. + */ + "clearable"?: boolean; + /** + * Closes the datepicker dropdown after selection + */ + "closeOnSelect"?: boolean; + /** + * The dropdown will close when the user interacts outside of this element (e.g. clicking). + */ + "containingElement"?: HTMLElement; + /** + * Define the dateFormat. Valid formats are: 'dd.mm.yyyy' 'yyyy-mm-dd' 'dd-mm-yyyy' 'dd/mm/yyyy' 'yyyy/mm/dd' 'dd.mm.yy' 'yy-mm-dd' 'dd-mm-yy' 'dd/mm/yy' 'yy/mm/dd' + */ + "dateFormat"?: SixDateFormats; + /** + * Set the amount of time, in milliseconds, to wait to trigger the `dateChange` event after each keystroke. + */ + "debounce"?: number; + /** + * The date to defines where the datepicker popup starts. The prop accepts ISO 8601 date strings (YYYY-MM-DD). + */ + "defaultDate"?: string; + /** + * If `true` the component is disabled. + */ + "disabled"?: boolean; + /** + * The error message shown, if `invalid` is set to true. + */ + "errorText"?: string | string[]; + /** + * The number of error texts to be shown (if the error-text slot isn't used). Defaults to 1 + */ + "errorTextCount"?: number; + /** + * Enable this option to prevent the panel from being clipped when the component is placed inside a container with `overflow: auto|scroll`. + */ + "hoist"?: boolean; + /** + * Set the position of the icon + */ + "iconPosition"?: 'left' | 'right'; + /** + * Indicates whether or not the calendar should be shown as an inline (always open) component + */ + "inline"?: boolean; + /** + * If this property is set to true and an error message is provided by `errorText`, the error message is displayed. + */ + "invalid"?: boolean; + /** + * The label text. + */ + "label"?: string; + /** + * The language used to render the weekdays and months. + */ + "locale"?: 'en' | 'de' | 'fr' | 'it' | 'es'; + /** + * The maximum datetime allowed. Value must be a date object + */ + "max"?: Date; + /** + * The minimum datetime allowed. Value must be a date object + */ + "min"?: Date; + /** + * The input's name attribute. + */ + "name"?: string; + /** + * Emitted when a option got selected. + */ + "onSix-datepicker-blur"?: (event: SixDateCustomEvent) => void; + /** + * Emitted when the clear button is activated. + */ + "onSix-datepicker-clear"?: (event: SixDateCustomEvent) => void; + /** + * Emitted when a option got selected. + */ + "onSix-datepicker-select"?: (event: SixDateCustomEvent) => void; + /** + * Indicates whether or not the calendar dropdown is open on startup. You can use this in lieu of the show/hide methods. + */ + "open"?: boolean; + /** + * The placeholder defines what text to be shown on the input element + */ + "placeholder"?: string; + /** + * The enforced placement of the dropdown panel. + */ + "placement"?: 'top' | 'bottom'; + /** + * If `true` the user can only select a date via the component in the popup, but not directly edit the input field. + */ + "readonly"?: boolean; + /** + * Set to true to show an asterisk beneath the label. + */ + "required"?: boolean; + /** + * Datepicker size. + */ + "size"?: 'small' | 'medium' | 'large'; + /** + * Set the type. + */ + "type"?: 'date' | 'date-time'; + /** + * The value of the form field, which accepts a date object. + */ + "value"?: Date; + } /** * @since 1.0 * @status stable @@ -3321,7 +3601,7 @@ declare namespace LocalJSX { /** * Define the dateFormat. Valid formats are: 'dd.mm.yyyy' 'yyyy-mm-dd' 'dd-mm-yyyy' 'dd/mm/yyyy' 'yyyy/mm/dd' 'dd.mm.yy' 'yy-mm-dd' 'dd-mm-yy' 'dd/mm/yy' 'yy/mm/dd' */ - "dateFormat"?: SixDateFormats; + "dateFormat"?: SixDateFormats1; /** * Set the amount of time, in milliseconds, to wait to trigger the `dateChange` event after each keystroke. */ @@ -3381,7 +3661,7 @@ declare namespace LocalJSX { /** * Emitted when a option got selected. */ - "onSix-datepicker-blur"?: (event: SixDatepickerCustomEvent) => void; + "onSix-datepicker-blur"?: (event: SixDatepickerCustomEvent) => void; /** * Emitted when the clear button is activated. */ @@ -3389,7 +3669,7 @@ declare namespace LocalJSX { /** * Emitted when a option got selected. */ - "onSix-datepicker-select"?: (event: SixDatepickerCustomEvent) => void; + "onSix-datepicker-select"?: (event: SixDatepickerCustomEvent) => void; /** * Indicates whether or not the calendar dropdown is open on startup. You can use this in lieu of the show/hide methods. */ @@ -5184,6 +5464,7 @@ declare namespace LocalJSX { "six-button": SixButton; "six-card": SixCard; "six-checkbox": SixCheckbox; + "six-date": SixDate; "six-datepicker": SixDatepicker; "six-details": SixDetails; "six-dialog": SixDialog; @@ -5271,6 +5552,11 @@ declare module "@stencil/core" { * Forked from https://github.com/shoelace-style/shoelace version v2.0.0-beta27. */ "six-checkbox": LocalJSX.SixCheckbox & JSXBase.HTMLAttributes; + /** + * @since 1.0 TODO: replace with correct version + * @status stable + */ + "six-date": LocalJSX.SixDate & JSXBase.HTMLAttributes; /** * @since 1.0 * @status stable diff --git a/libraries/ui-library/src/components/six-date/components/day-selection.tsx b/libraries/ui-library/src/components/six-date/components/day-selection.tsx new file mode 100644 index 000000000..321591a86 --- /dev/null +++ b/libraries/ui-library/src/components/six-date/components/day-selection.tsx @@ -0,0 +1,42 @@ +import { h } from '@stencil/core'; +import { DateLocale } from '../../../utils/date-util'; +import { CalendarCell } from '../six-date'; + +interface DaySelectionParams { + locale: DateLocale; + calendarGrid: CalendarCell[][]; + onClickDateCell: (cell: CalendarCell) => void; +} +export const DaySelection = (daySelectionParams: DaySelectionParams) => { + return ( + + + {daySelectionParams.locale.weekdaysMin.map((weekday) => ( + + ))} + + + {daySelectionParams.calendarGrid.map((row) => ( + + {row.map((cell) => ( + + ))} + + ))} + +
{weekday}
daySelectionParams.onClickDateCell(cell)} + class={{ + 'datepicker-table__cell': true, + 'datepicker-table__cell--is-today': cell.isToday, + 'datepicker-table__cell--is-selected': cell.isSelected, + 'datepicker-table__cell--is-outdated': cell.isOutdated, + 'datepicker-table__cell--is-disabled': cell.isDisabled, + 'datepicker-table__cell--is-selectable': !cell.isDisabled, + }} + > + {cell.label} +
+ ); +}; diff --git a/libraries/ui-library/src/components/six-date/components/month-selection.tsx b/libraries/ui-library/src/components/six-date/components/month-selection.tsx new file mode 100644 index 000000000..08717dcb2 --- /dev/null +++ b/libraries/ui-library/src/components/six-date/components/month-selection.tsx @@ -0,0 +1,38 @@ +import { h } from '@stencil/core'; +import { DateLocale, now } from '../../../utils/date-util'; + +interface MonthSelectionParams { + locale: DateLocale; + selectedDate?: Date; + onClickMonthCell: (month: string) => void; +} +export const MonthSelection = (monthSelectionParams: MonthSelectionParams) => { + const locale = monthSelectionParams.locale; + const isToday = (value: string) => locale.monthsShort[now().getMonth()] === value; + + const isSelectedMonth = (value: string) => + monthSelectionParams.selectedDate?.getMonth() === locale.monthsShort.findIndex((m) => m === value); + + return ( + + + {locale.monthsShortGrouped.map((row: string[]) => ( + + {row.map((month) => ( + + ))} + + ))} + +
monthSelectionParams.onClickMonthCell(month)} + class={{ + 'datepicker-table__cell': true, + 'datepicker-table__cell--is-today': isToday(month), + 'datepicker-table__cell--is-selected': isSelectedMonth(month), + }} + > + {month} +
+ ); +}; diff --git a/libraries/ui-library/src/components/six-date/components/year-selection.tsx b/libraries/ui-library/src/components/six-date/components/year-selection.tsx new file mode 100644 index 000000000..6a9e0b5e5 --- /dev/null +++ b/libraries/ui-library/src/components/six-date/components/year-selection.tsx @@ -0,0 +1,32 @@ +import { now } from '../../../utils/date-util'; +import { h } from '@stencil/core'; + +interface YearSelectionParams { + selectedDate?: Date; + yearSelection: number[][]; + onClickYearCell: (year: number) => void; +} +export const YearSelection = (yearSelectionParams: YearSelectionParams) => { + return ( + + + {yearSelectionParams.yearSelection.map((row) => ( + + {row.map((year) => ( + + ))} + + ))} + +
yearSelectionParams.onClickYearCell(year)} + class={{ + 'datepicker-table__cell': true, + 'datepicker-table__cell--is-today': now().getFullYear() === year, + 'datepicker-table__cell--is-selected': yearSelectionParams.selectedDate?.getFullYear() === year, + }} + > + {year} +
+ ); +}; diff --git a/libraries/ui-library/src/components/six-date/index.html b/libraries/ui-library/src/components/six-date/index.html new file mode 100644 index 000000000..5780c64fb --- /dev/null +++ b/libraries/ui-library/src/components/six-date/index.html @@ -0,0 +1,564 @@ + + + + + + + + + six-date + + +
+

Datepickers allow the user to manually enter a date or open a popup calendar to select a date.

+
+ +
+ +

Examples

+ +

Reading selected date

+

+ Either manually enter a date in the input field or select a date from the popup. You can read the selected date + by listening to the six-datepicker-select event on the datepicker The selected date will be shown + below the datepicker. +

+
+ +
No Date selected yet!
+ + + +
+ +

Date and Time

+

+ If you need to select a time as well, just set the type and date-format and the + datepicker will include a timepicker too. +

+
+ +
No Date selected yet!
+ + + +
+ +

Disabled State

+
+ + +
+ +

Inline State

+

If you wish you can have the datepicker as an inline component i.e. the popup will always be open

+
+ + +
+ +

Readonly State

+

+ If you want to force the user to use the popup instead of manually enter a date, you can simply add the + readonly property +

+
+ + +
+ +

Placement

+

+ By default the datepicker will render either below or above the input field depending on the available space. +

+

If you want to enforce a certain placement you can use the placement attribute

+
+ + +
+ +

Placeholder

+

You can define the datepickers placeholder via placeholder

+
+ + +
+ +

Locale

+

You can change the language in which the datepicker is shown by using the locale attribute

+
+ + +
+ +

Footer

+

You can add content to the footer by simply using the datepicker's slot

+
+ +

Choose your birthdate!

+
+ +
+ +

Of course you can also enhance the datepicker footer with special functions

+ +
+ +
+ Today + Clear +
+
+ + +
+ +

Date Format

+ +

The SIX datepicker supports the following different formats:

+
+
+ + + + + + + + + + +
+ + Apply dates + + + +
+ +

Define Min & Max

+ +

You can set the min and max dates. Please be aware that only date objects are accepted and n:

+ +
+
+ + + +
+ +
+ Selected: No Date selected yet! + +
+ + + + +
+ +

Allowed Dates

+ +

+ If the set of allowed dates is more complex than just passing a min and max date you can define a allow dates + callback function +

+ +
+ + + + +
+ +

Custom Start Date

+ +

You can define a custom default date which will be shown on first appearance of the popup

+ +
+ + +
+ +

Clearable

+

Add the clearable property to add a clear button when the input has content.

+ +
+ + +
+ +

Icon position

+

Could be adjusted providing icon-position property.

+ +
+ +
+ +
+ + customicon + + +
+ +

Custom icon

+

The slot custom-icon is used to provide a custom datepicker icon.

+ +
+ + customicon + + +
+ +

Sizes

+

Use the size attribute to change a datepicker size.

+
+ +
+ +
+ + +
+ +

Hoisting

+

+ Dropdown panels will be clipped if they're inside a container that has overflow: auto|hidden. The hoist + attribute forces the panel to use a fixed positioning strategy, allowing it to break out of the container. In + this case, the panel will be positioned relative to its containing block, which is usually the viewport unless + an ancestor uses a transform, perspective, or filter. +

+
+
+ + +
+ +
+ +

Dynamic changes

+ +

The datepicker value can be changed dynamically by setting the value attribute.

+ +
+ +
+ Selected: No Date selected yet! +
+
+ 01.01.2020 + 15.06.3020 + 31.12.1420 +
+ + +
+ +

Open State

+

If you wish you can have the datepicker open on startup. It will close as usual (clicking outside etc.)

+
+ + +
+ +

Datepicker in a Modal

+ +
+ Toggle Modal + +
+ + +
+ + + + +
+ +

Error Text

+ +

Add a descriptive error message using either the error-text prop, or the equally named slot.

+ + + warning + There are two caveats when using the error-text prop/slot: +
    +
  1. + Remember to set the invalid prop as well! If you only provide some content to the + error-text prop/slot, it won't be shown unless the invalid prop is set to true +
  2. +
  3. + When using the prop, and you need to show more than one message, remember to also set the + error-text-count prop to a value that is the same or bigger than the length of the list of + messages you are using. Otherwise only one message will be shown at a time +
  4. +
+
+ +

The error-text prop accepts either a simple string message, or a list of messages.

+ +
+ + +
+ +
+ + +
+ +

+ When using the error-text slot, it is recommended to use the six-error component to + wrap the error message(s). This will provide the correct styling out of the box +

+
+ +
+ An error message + with a link +
+
+
+
+ + diff --git a/libraries/ui-library/src/components/six-date/readme.md b/libraries/ui-library/src/components/six-date/readme.md new file mode 100644 index 000000000..8689047d7 --- /dev/null +++ b/libraries/ui-library/src/components/six-date/readme.md @@ -0,0 +1,128 @@ +# six-datepicker + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | +| `allowedDates` | -- | Callback to determine which date in the datepicker should be selectable. the callback function will get a datestring as an argument, e.g. '2021-07-04' Usage e.g.: const datepicker = document.getElementById('allowed-date-picker'); datepicker.allowedDates = datestring => parseInt(datestring.split('-')[2], 10) % 2 === 0; | `(date: Date) => boolean` | `() => true` | +| `clearable` | `clearable` | Set to true to add a clear button when the input is populated. | `boolean` | `false` | +| `closeOnSelect` | `close-on-select` | Closes the datepicker dropdown after selection | `boolean` | `this.type === 'date'` | +| `containingElement` | -- | The dropdown will close when the user interacts outside of this element (e.g. clicking). | `HTMLElement \| undefined` | `undefined` | +| `dateFormat` | `date-format` | Define the dateFormat. Valid formats are: 'dd.mm.yyyy' 'yyyy-mm-dd' 'dd-mm-yyyy' 'dd/mm/yyyy' 'yyyy/mm/dd' 'dd.mm.yy' 'yy-mm-dd' 'dd-mm-yy' 'dd/mm/yy' 'yy/mm/dd' | `SixDateFormats.DDMMYYYY_DASH \| SixDateFormats.DDMMYYYY_DASH_TIME \| SixDateFormats.DDMMYYYY_SLASH \| SixDateFormats.DDMMYYYY_SLASH_TIME \| SixDateFormats.DDMMYYY_DOT \| SixDateFormats.DDMMYYY_DOT_TIME \| SixDateFormats.DDMMYY_DASH \| SixDateFormats.DDMMYY_DASH_TIME \| SixDateFormats.DDMMYY_DOT \| SixDateFormats.DDMMYY_DOT_TIME \| SixDateFormats.DDMMYY_SLASH \| SixDateFormats.DDMMYY_SLASH_TIME \| SixDateFormats.YYMMDD_DASH \| SixDateFormats.YYMMDD_DASH_TIME \| SixDateFormats.YYMMDD_SLASH \| SixDateFormats.YYMMDD_SLASH_TIME \| SixDateFormats.YYYYMMDD_DASH \| SixDateFormats.YYYYMMDD_DASH_TIME \| SixDateFormats.YYYYMMDD_SLASH \| SixDateFormats.YYYYMMDD_SLASH_TIME` | `SixDateFormats.DDMMYYY_DOT` | +| `debounce` | `debounce` | Set the amount of time, in milliseconds, to wait to trigger the `dateChange` event after each keystroke. | `number` | `DEFAULT_DEBOUNCE_FAST` | +| `defaultDate` | `default-date` | The date to defines where the datepicker popup starts. The prop accepts ISO 8601 date strings (YYYY-MM-DD). | `string \| undefined` | `undefined` | +| `disabled` | `disabled` | If `true` the component is disabled. | `boolean` | `false` | +| `errorText` | `error-text` | The error message shown, if `invalid` is set to true. | `string \| string[]` | `''` | +| `errorTextCount` | `error-text-count` | The number of error texts to be shown (if the error-text slot isn't used). Defaults to 1 | `number \| undefined` | `undefined` | +| `hoist` | `hoist` | Enable this option to prevent the panel from being clipped when the component is placed inside a container with `overflow: auto\|scroll`. | `boolean` | `false` | +| `iconPosition` | `icon-position` | Set the position of the icon | `"left" \| "right"` | `'left'` | +| `inline` | `inline` | Indicates whether or not the calendar should be shown as an inline (always open) component | `boolean` | `false` | +| `invalid` | `invalid` | If this property is set to true and an error message is provided by `errorText`, the error message is displayed. | `boolean` | `false` | +| `label` | `label` | The label text. | `string` | `''` | +| `locale` | `locale` | The language used to render the weekdays and months. | `"de" \| "en" \| "es" \| "fr" \| "it"` | `'en'` | +| `max` | -- | The maximum datetime allowed. Value must be a date object | `Date \| undefined` | `undefined` | +| `min` | -- | The minimum datetime allowed. Value must be a date object | `Date \| undefined` | `undefined` | +| `name` | `name` | The input's name attribute. | `string` | `''` | +| `open` | `open` | Indicates whether or not the calendar dropdown is open on startup. You can use this in lieu of the show/hide methods. | `boolean` | `false` | +| `placeholder` | `placeholder` | The placeholder defines what text to be shown on the input element | `string \| undefined` | `undefined` | +| `placement` | `placement` | The enforced placement of the dropdown panel. | `"bottom" \| "top" \| undefined` | `undefined` | +| `readonly` | `readonly` | If `true` the user can only select a date via the component in the popup, but not directly edit the input field. | `boolean` | `false` | +| `required` | `required` | Set to true to show an asterisk beneath the label. | `boolean` | `false` | +| `size` | `size` | Datepicker size. | `"large" \| "medium" \| "small"` | `'medium'` | +| `type` | `type` | Set the type. | `"date" \| "date-time"` | `'date'` | +| `value` | -- | The value of the form field, which accepts a date object. | `Date \| undefined` | `undefined` | + + +## Events + +| Event | Description | Type | +| ----------------------- | ------------------------------------------- | ---------------------------------------- | +| `six-datepicker-blur` | Emitted when a option got selected. | `CustomEvent` | +| `six-datepicker-clear` | Emitted when the clear button is activated. | `CustomEvent` | +| `six-datepicker-select` | Emitted when a option got selected. | `CustomEvent` | + + +## Methods + +### `select(datestring?: string) => Promise` + +Selects an option + +#### Parameters + +| Name | Type | Description | +| ------------ | --------------------- | ----------- | +| `datestring` | `string \| undefined` | | + +#### Returns + +Type: `Promise` + + + +### `setFocus(options?: FocusOptions) => Promise` + +Sets focus on the datepickers input. + +#### Parameters + +| Name | Type | Description | +| --------- | --------------------------- | ----------- | +| `options` | `FocusOptions \| undefined` | | + +#### Returns + +Type: `Promise` + + + + +## Slots + +| Slot | Description | +| -------------- | ----------------------------------------------------------------------------------------------- | +| | Used to define a footer for the date picker. | +| `"error-text"` | Error text that is shown for validation errors. Alternatively, you can use the error-text prop. | + + +## Shadow Parts + +| Part | Description | +| ---------- | ----------- | +| `"base"` | | +| `"header"` | | +| `"icon"` | | +| `"popup"` | | + + +## Dependencies + +### Depends on + +- [six-icon](../six-icon) +- [six-input](../six-input) +- [six-timepicker](../six-timepicker) + +### Graph +```mermaid +graph TD; + six-datepicker --> six-icon + six-datepicker --> six-input + six-datepicker --> six-timepicker + six-input --> six-icon + six-input --> six-error + six-timepicker --> six-item-picker + six-timepicker --> six-icon + six-timepicker --> six-input + six-item-picker --> six-icon + style six-datepicker fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +Copyright © 2021-present SIX-Group diff --git a/libraries/ui-library/src/components/six-date/six-date-formats.tsx b/libraries/ui-library/src/components/six-date/six-date-formats.tsx new file mode 100644 index 000000000..af1f6a867 --- /dev/null +++ b/libraries/ui-library/src/components/six-date/six-date-formats.tsx @@ -0,0 +1,24 @@ +// eslint-disable-next-line @stencil-community/ban-exported-const-enums +export enum SixDateFormats { + DDMMYYY_DOT = 'dd.mm.yyyy', + YYYYMMDD_DASH = 'yyyy-mm-dd', + DDMMYYYY_DASH = 'dd-mm-yyyy', + DDMMYYYY_SLASH = 'dd/mm/yyyy', + YYYYMMDD_SLASH = 'yyyy/mm/dd', + DDMMYY_DOT = 'dd.mm.yy', + YYMMDD_DASH = 'yy-mm-dd', + DDMMYY_DASH = 'dd-mm-yy', + DDMMYY_SLASH = 'dd/mm/yy', + YYMMDD_SLASH = 'yy/mm/dd', + + DDMMYYY_DOT_TIME = 'dd.mm.yyyy hh:MM:ss', + YYYYMMDD_DASH_TIME = 'yyyy-mm-dd hh:MM:ss', + DDMMYYYY_DASH_TIME = 'dd-mm-yyyy hh:MM:ss', + DDMMYYYY_SLASH_TIME = 'dd/mm/yyyy hh:MM:ss', + YYYYMMDD_SLASH_TIME = 'yyyy/mm/dd hh:MM:ss', + DDMMYY_DOT_TIME = 'dd.mm.yy hh:MM:ss', + YYMMDD_DASH_TIME = 'yy-mm-dd hh:MM:ss', + DDMMYY_DASH_TIME = 'dd-mm-yy hh:MM:ss', + DDMMYY_SLASH_TIME = 'dd/mm/yy hh:MM:ss', + YYMMDD_SLASH_TIME = 'yy/mm/dd hh:MM:ss', +} diff --git a/libraries/ui-library/src/components/six-date/six-date.scss b/libraries/ui-library/src/components/six-date/six-date.scss new file mode 100644 index 000000000..c35263d1e --- /dev/null +++ b/libraries/ui-library/src/components/six-date/six-date.scss @@ -0,0 +1,178 @@ +@import 'src/global/component'; + +:host { + display: block; + font-family: var(--six-font-family); +} + +.datepicker { + &__container { + position: relative; + } + + &__popup { + min-width: 400px; + background-color: white; + padding: 0.5em 0.5em 1.5em; + box-shadow: 0 2px 4px -1px rgb(0 0 0 / 20%), 0 4px 5px 0 rgb(0 0 0 / 14%), 0 1px 10px 0 rgb(0 0 0 / 12%); + user-select: none; + position: absolute; + z-index: var(--six-z-index-dropdown); + right: 0; + left: 0; + + &--is-up { + bottom: 100%; + // fix problem where you would need to jump around with the cursor when switching months and + // its a dropup since not all months have the same height + min-height: 382px; + } + + &--is-inline { + position: initial; + box-shadow: none; + border: 1px solid var(--six-color-web-rock-400); + } + } +} + +.datepicker-header { + display: flex; + justify-content: space-between; + border-bottom: 1px solid var(--six-color-web-rock-400); + padding: 0.5em; + margin-bottom: 1em; + + & svg { + fill: var(--six-color-web-rock-900); + stroke: none; + } + + &__btn { + cursor: pointer; + width: 2.5em; + height: 2.5em; + display: flex; + vertical-align: middle; + align-items: center; + justify-content: center; + + &:hover { + background-color: var(--six-color-web-rock-100); + border-radius: 100%; + } + } + + &__label { + display: flex; + + & > div { + cursor: pointer; + padding: 0.5em; + display: flex; + vertical-align: middle; + align-items: center; + justify-content: center; + + &:hover { + background-color: var(--six-color-web-rock-100); + } + } + } +} + +.datepicker-table { + width: 100%; + + &__cell { + text-align: center; + width: 2.5rem; + height: 2.5rem; + cursor: pointer; + + &:hover { + background-color: var(--six-color-web-rock-100); + } + + &--is-today { + border: 1px solid var(--six-color-web-rock-400); + } + + &--is-selected { + background-color: var(--six-color-red); + color: var(--six-color-white); + } + + &--is-disabled { + background-color: var(--six-color-web-rock-200); + cursor: initial; + + &:hover { + background-color: var(--six-color-web-rock-200); + } + } + + &--is-outdated { + color: var(--six-color-web-rock-400); + } + } +} + +// Clearable +.datepicker-clear { + display: inline-flex; + align-items: center; + font-size: inherit; + color: var(--six-input-icon-color); + border: none; + background: none; + padding: 0; + transition: var(--six-transition-fast) color; + cursor: pointer; + + &:hover { + color: var(--six-input-icon-color-hover); + } + + &:focus { + outline: none; + } + + &--right { + right: 0; + position: absolute; + } + + &--left { + right: 35px; + position: absolute; + } +} + +.input--empty .datepicker-clear { + visibility: hidden; +} + +// Icon position +.prefix { + cursor: pointer; + + &--right { + right: 0; + display: inline-flex; + position: absolute; + font-size: inherit; + color: var(--six-input-icon-color); + border: none; + background: none; + margin-right: var(--six-input-spacing-medium); + transition: var(--six-transition-fast) color; + } +} + +// time-picker +six-timepicker::part(popup) { + border: none; + padding: 0.5em 0.5em 0; + min-height: 0; +} diff --git a/libraries/ui-library/src/components/six-date/six-date.tsx b/libraries/ui-library/src/components/six-date/six-date.tsx new file mode 100644 index 000000000..7c5802a18 --- /dev/null +++ b/libraries/ui-library/src/components/six-date/six-date.tsx @@ -0,0 +1,821 @@ +import { Component, Element, Event, EventEmitter, h, Listen, Method, Prop, State, Watch } from '@stencil/core'; +import { + createCalendarGrid, + day, + formatDate, + getFirstDayOfTheWeek, + hours, + i18nDate, + isValidDate, + isValidDateString, + minutes, + month, + now, + PointerDate, + rangeAround, + removeTime, + seconds, + toDate, + year, +} from '../../utils/date-util'; +import { EventListeners } from '../../utils/event-listeners'; +import { debounce, debounceEvent, DEFAULT_DEBOUNCE_FAST } from '../../utils/execution-control'; +import { hasSlot } from '../../utils/slot'; +import { EmptyPayload } from '../../utils/types'; +import { SixDateFormats } from './six-date-formats'; +import { MonthSelection } from './components/month-selection'; +import { DaySelection } from './components/day-selection'; +import { YearSelection } from './components/year-selection'; +import { SixTimepickerChange } from '../six-timepicker/six-timepicker'; +import Popover from '../../utils/popover'; + +const NUMBER_OF_YEARS_SHOWN = 25; + +const MIN_POPUP_HEIGHT = 400; + +export type SixDatepickerSelectPayload = Date | undefined | null; + +export interface CalendarCell { + date: Date; + dateString: string; + display: string; + isDisabled: boolean; + isOutdated: boolean; + isSelected: boolean; + isToday: boolean; + label: string; +} + +enum SelectionMode { + DAY = 'day', + MONTH = 'month', + YEAR = 'year', +} + +/** + * @since 1.0 TODO: replace with correct version + * @status stable + * + * @slot - Used to define a footer for the date picker. + * @slot error-text - Error text that is shown for validation errors. Alternatively, you can use the error-text prop. + */ +@Component({ + tag: 'six-date', + styleUrl: 'six-date.scss', + shadow: true, +}) +export class SixDate { + private eventListeners = new EventListeners(); + private inputElement?: HTMLSixInputElement; + private popup?: HTMLElement; + private wrapper?: HTMLElement; + private selectedDate?: Date; + private popover?: Popover; + + @Element() host!: HTMLSixDateElement; + + @State() private pointerDate = SixDate.getCurrentDateAsPointer(); + @State() selectionMode: SelectionMode = SelectionMode.DAY; + @State() isDropDownContentUp = false; + + /** + * Set the type. + */ + @Prop() type: 'date' | 'date-time' = 'date'; + + /** + * The language used to render the weekdays and months. + */ + @Prop() locale: 'en' | 'de' | 'fr' | 'it' | 'es' = 'en'; + + /** Indicates whether or not the calendar dropdown is open on startup. You can use this in lieu of the show/hide methods. */ + @Prop({ mutable: true, reflect: true }) open = false; + + /** Indicates whether or not the calendar should be shown as an inline (always open) component */ + @Prop({ reflect: true }) inline = false; + + /** + * If `true` the user can only select a date via the component in the popup, but not directly edit the input field. + */ + @Prop() readonly = false; + + /** + * If `true` the component is disabled. + */ + @Prop() disabled = false; + + /** + * Callback to determine which date in the datepicker should be selectable. + * the callback function will get a datestring as an argument, e.g. '2021-07-04' + * + * Usage e.g.: + * const datepicker = document.getElementById('allowed-date-picker'); + * datepicker.allowedDates = datestring => parseInt(datestring.split('-')[2], 10) % 2 === 0; + */ + @Prop() allowedDates: (date: Date) => boolean = () => true; + + /** + * The minimum datetime allowed. Value must be a date object + */ + @Prop() min?: Date; + + /** + * The maximum datetime allowed. Value must be a date object + */ + @Prop() max?: Date; + + /** + * Closes the datepicker dropdown after selection + */ + @Prop() closeOnSelect = this.type === 'date'; + + /** + * The enforced placement of the dropdown panel. + */ + @Prop() placement?: 'top' | 'bottom'; + + /** Datepicker size. */ + @Prop() size: 'small' | 'medium' | 'large' = 'medium'; + + /** Set to true to show an asterisk beneath the label. */ + @Prop() required = false; + + /** + * The date to defines where the datepicker popup starts. The prop accepts ISO 8601 date strings (YYYY-MM-DD). + */ + @Prop() defaultDate?: string; + + /** + * The placeholder defines what text to be shown on the input element + */ + @Prop() placeholder?: string; + + /** + * The value of the form field, which accepts a date object. + */ + @Prop({ mutable: true }) value?: Date; + + /** The label text. */ + @Prop() label = ''; + + /** The error message shown, if `invalid` is set to true. */ + @Prop() errorText: string | string[] = ''; + + /** The number of error texts to be shown (if the error-text slot isn't used). Defaults to 1 */ + @Prop() errorTextCount?: number; + + /** If this property is set to true and an error message is provided by `errorText`, the error message is displayed. */ + @Prop({ reflect: true }) invalid = false; + + /** The dropdown will close when the user interacts outside of this element (e.g. clicking). */ + @Prop() containingElement?: HTMLElement; + + /** Define the dateFormat. Valid formats are: + * 'dd.mm.yyyy' + * 'yyyy-mm-dd' + * 'dd-mm-yyyy' + * 'dd/mm/yyyy' + * 'yyyy/mm/dd' + * 'dd.mm.yy' + * 'yy-mm-dd' + * 'dd-mm-yy' + * 'dd/mm/yy' + * 'yy/mm/dd' + * */ + @Prop() dateFormat: SixDateFormats = SixDateFormats.DDMMYYY_DOT; + + /** + * Set the amount of time, in milliseconds, to wait to trigger the `dateChange` event after each keystroke. + */ + @Prop() debounce = DEFAULT_DEBOUNCE_FAST; + + /** The input's name attribute. */ + @Prop({ reflect: true }) name = ''; + + /** Set to true to add a clear button when the input is populated. */ + @Prop() clearable = false; + + /** Set the position of the icon */ + @Prop() iconPosition: 'left' | 'right' = 'left'; + + /** + * Enable this option to prevent the panel from being clipped when the component is placed inside a container with + * `overflow: auto|scroll`. + */ + @Prop() hoist = false; + + @Watch('debounce') + protected debounceChanged() { + this.sixSelect = debounceEvent(this.sixSelect, this.debounce); + } + + /** + * Update the native input element when the value changes + */ + @Watch('value') + protected valueChanged() { + if (this.value != null && !isValidDate(this.value)) { + console.warn('invalid date value: ', this.value); + this.value = undefined; + this.sixSelect.emit(this.value); + } + this.selectedDate = this.value; + this.updatePointerDates(); + } + + /** + * Emitted when a option got selected. + */ + @Event({ eventName: 'six-datepicker-select' }) sixSelect!: EventEmitter; + + /** + * Emitted when the clear button is activated. + */ + @Event({ eventName: 'six-datepicker-clear' }) sixClear!: EventEmitter; + + /** + * Emitted when a option got selected. + */ + @Event({ eventName: 'six-datepicker-blur' }) sixBlur!: EventEmitter; + + @Listen('resize', { target: 'window' }) + async resizeHandler() { + this.updateDropdownDirection(); + this.moveOpenHoistedPopup(); + this.adjustPopupPosition(); + } + + @Listen('scroll', { target: 'window' }) + async scrollHandler() { + this.updateDropdownDirection(); + this.moveOpenHoistedPopup(); + } + + private moveOpenHoistedPopup() { + this.popover?.reposition(); + // TODO + // movePopup(this.hoist, this.open, this.popup, this.inputElement, this.wrapper, MIN_POPUP_HEIGHT); + } + + get container() { + return this.containingElement || this.host; + } + + get firstDateOfBox(): Date { + const date = new Date(this.pointerDate.year, this.pointerDate.month, 1); + return getFirstDayOfTheWeek(date); + } + + /** Sets focus on the datepickers input. */ + @Method() + async setFocus(options?: FocusOptions) { + this.inputElement?.setFocus(options); + } + + get calendarGrid() { + return createCalendarGrid({ + firstDateOfBox: this.firstDateOfBox, + allowedDates: this.allowedDates, + dateFormat: this.dateFormat, + locale: this.locale, + selectedDate: this.selectedDate, + minDate: this.min, + maxDate: this.max, + pointerDate: this.pointerDate, + }); + } + + private updateDropdownDirection() { + if (this.inputElement == null || this.wrapper == null) { + return; + } + // TODO + //this.isDropDownContentUp = calcIsDropDownContentUp(this.inputElement, this.wrapper, MIN_POPUP_HEIGHT); + } + + private getMonthStringForIndex(index: number) { + return i18nDate[this.locale].months[index]; + } + + private previousUnit = () => { + if (this.selectionMode === SelectionMode.DAY) { + this.previousMonth(); + } else if (this.selectionMode === SelectionMode.MONTH) { + this.previousYear(); + } else if (this.selectionMode === SelectionMode.YEAR) { + this.previousYearGroup(); + } + }; + + private previousYear() { + this.pointerDate = { ...this.pointerDate, year: this.pointerDate.year - 1 }; + } + + private previousYearGroup() { + this.pointerDate = { ...this.pointerDate, year: this.pointerDate.year - NUMBER_OF_YEARS_SHOWN }; + } + + private previousMonth() { + if (this.pointerDate.month === 0) { + this.pointerDate = { year: this.pointerDate.year - 1, month: 11, day: 1, hours: 0, minutes: 0, seconds: 0 }; + } else { + this.pointerDate = { + year: this.pointerDate.year, + month: this.pointerDate.month - 1, + day: 1, + hours: 0, + minutes: 0, + seconds: 0, + }; + } + } + + private nextUnit = () => { + if (this.selectionMode === SelectionMode.DAY) { + this.nextMonth(); + } else if (this.selectionMode === SelectionMode.MONTH) { + this.nextYear(); + } else if (this.selectionMode === SelectionMode.YEAR) { + this.nextYearGroup(); + } + }; + + private nextMonth() { + if (this.pointerDate.month === 11) { + this.pointerDate = { year: this.pointerDate.year + 1, month: 0, day: 1, hours: 0, minutes: 0, seconds: 0 }; + } else { + this.pointerDate = { + year: this.pointerDate.year, + month: this.pointerDate.month + 1, + day: 1, + hours: 0, + minutes: 0, + seconds: 0, + }; + } + } + + private nextYear() { + this.pointerDate = { ...this.pointerDate, year: this.pointerDate.year + 1 }; + } + + private nextYearGroup() { + this.pointerDate = { ...this.pointerDate, year: this.pointerDate.year + NUMBER_OF_YEARS_SHOWN }; + } + + private openCalendar() { + if (!this.open && !this.disabled) { + this.open = true; + this.setupEventListenersForOpenPopup(); + } + } + + private setupEventListenersForOpenPopup() { + this.eventListeners.add(document, 'keydown', this.handleDocumentKeyDown); + this.eventListeners.add(document, 'mousedown', this.handleDocumentMouseDown); + } + + private handleDocumentKeyDown = (event: Event) => { + const keyboardEvent = event as KeyboardEvent; + // Close when escape is pressed + if (this.open && keyboardEvent.key === 'Escape') { + keyboardEvent.stopPropagation(); + this.closePopup(); + void this.inputElement?.setFocus(); + } + + // Handle tabbing + if (keyboardEvent.key === 'Tab') { + this.closePopup(); + } + }; + + private handleDocumentMouseDown = (event: Event) => { + // Close when clicking outside the containing element + const path = event.composedPath() as EventTarget[]; + if (!path.includes(this.container)) { + this.closePopup(); + return; + } + }; + + private handleClearClick = async (event: MouseEvent) => { + event.stopPropagation(); + await this.select(undefined); + this.sixClear.emit(); + }; + + private closePopup() { + if (this.inline) { + return; + } + + this.open = false; + this.eventListeners.remove(document, 'keydown', this.handleDocumentKeyDown); + this.eventListeners.remove(document, 'mousedown', this.handleDocumentMouseDown); + this.selectionMode = SelectionMode.DAY; + } + + private updatePointerDates() { + const date = this.getPointerDate(); + if (this.differsFromPointerDate(date)) { + this.pointerDate = { + year: year(date), + month: month(date), + day: day(date), + hours: hours(date), + minutes: minutes(date), + seconds: seconds(date), + }; + } + } + + private differsFromPointerDate(date?: Date): boolean { + return ( + this.pointerDate.day !== day(date) || + this.pointerDate.month !== month(date) || + this.pointerDate.year !== year(date) || + this.pointerDate.hours !== hours(date) || + this.pointerDate.minutes !== minutes(date) || + this.pointerDate.seconds !== seconds(date) + ); + } + + private getPointerDate(): Date | undefined { + if (this.selectedDate !== undefined && this.selectedDate !== null) { + return this.selectedDate; + } + if (this.defaultDate == null) { + return this.type === 'date' ? removeTime(now()) : now(); + } else { + return toDate(this.defaultDate, this.dateFormat); + } + } + + private updateValue(newDate?: Date) { + this.updateIfChanged(newDate); + } + + private updateIfChanged(newDate?: Date) { + if (this.value?.getTime() === newDate?.getTime()) { + return; + } + this.value = newDate; + this.sixSelect.emit(this.value); + } + + /** + * Selects an option + */ + @Method() + async select(datestring?: string) { + if (datestring == null) { + this.updateValue(undefined); + } else { + const newDate = toDate(datestring, this.dateFormat); + newDate?.setHours(this.pointerDate.hours, this.pointerDate.minutes, this.pointerDate.seconds); + this.updateValue(newDate); + } + + this.updatePointerDates(); + + if (this.closeOnSelect) { + this.closePopup(); + } + } + + private onTimepickerChange = (sixTimepickerChange: CustomEvent) => { + const time = sixTimepickerChange.detail.value; + const newDate = new Date(); + + if (this.selectedDate != null) { + newDate.setFullYear(this.selectedDate.getFullYear(), this.selectedDate.getMonth(), this.selectedDate.getDate()); + } + + if (time != null) { + const hours = time.hours; + const minutes = time.minutes; + const seconds = time.seconds; + if (hours != null) { + newDate.setHours(hours, minutes, seconds); + } + } + + this.updateValue(newDate); + this.updatePointerDates(); + }; + + private onClickDateCell = (cell: CalendarCell) => { + if (!cell.isDisabled) { + void this.select(cell.dateString); + } + }; + + private onClickMonthCell = (selectedMonth: string) => { + const month = i18nDate[this.locale].monthsShort.findIndex((monthShort) => monthShort === selectedMonth); + this.pointerDate = { ...this.pointerDate, month }; + this.selectionMode = SelectionMode.DAY; + }; + + private onClickYearCell = (year: number) => { + this.pointerDate = { ...this.pointerDate, year }; + this.selectionMode = SelectionMode.DAY; + }; + + private handleInputChange = (event: Event) => { + if (this.inputElement == null) { + return; + } + event.stopPropagation(); + + const inputValue = this.inputElement.value; + if (!isValidDateString(inputValue, this.dateFormat)) { + return; + } + + const inputValueDate = toDate(inputValue, this.dateFormat); + + if (inputValueDate === undefined) { + return; + } + + this.updateIfChanged(inputValueDate); + const datesOnly = inputValue.replace(/[^\d]/g, ''); + if (datesOnly.length >= 6) { + const date = toDate(inputValue, this.dateFormat); + const dateAsString = formatDate(date, this.dateFormat); + if (isValidDateString(dateAsString, this.dateFormat)) { + this.selectedDate = toDate(dateAsString, this.dateFormat); + this.updatePointerDates(); + this.updateValue(this.selectedDate); + } + } + }; + + private handleOnBlur = (event: Event) => { + // clear the value if the user deleted the date + if (this.inputElement?.value === '' && isValidDate(this.value)) { + this.value = undefined; + this.sixSelect.emit(this.value); + } + + event.stopPropagation(); + const inputValue = this.inputElement?.value; + const inputValueDate = toDate(inputValue, this.dateFormat); + const formattedDate = formatDate(this.value, this.dateFormat); + + if (this.inputElement != null && inputValueDate != null && inputValue !== formattedDate) { + // properly format date if necessary + this.inputElement.value = formattedDate; + } + + this.sixBlur.emit(this.value); + }; + + componentWillLoad() { + this.selectedDate = this.value; + this.updatePointerDates(); + this.updateValue(this.value); + this.popover = new Popover(this.host, this.popup!, {}); + + if (this.inline) { + this.open = true; + } + + if (this.open) { + this.setupEventListenersForOpenPopup(); + } + } + + componentDidLoad() { + if (this.inputElement != null) { + this.eventListeners.add(this.inputElement, 'six-input-input', debounce(this.handleInputChange, this.debounce)); + this.eventListeners.add(this.inputElement, 'six-input-blur', this.handleOnBlur); + } + } + + componentDidRender() { + this.adjustPopupPosition(); + } + + private renderHeader() { + return ( +
+ + +
+ {this.selectionMode === SelectionMode.DAY && ( +
(this.selectionMode = SelectionMode.MONTH)}> + {this.getMonthStringForIndex(this.pointerDate.month)} + + + + + +
+ )} + + {this.selectionMode !== SelectionMode.YEAR && ( +
(this.selectionMode = SelectionMode.YEAR)}> + {this.pointerDate.year} + + + + + +
+ )} + + {this.selectionMode === SelectionMode.YEAR && ( +
+ {this.pointerDate.year - Math.floor(NUMBER_OF_YEARS_SHOWN / 2)} –{' '} + {this.pointerDate.year + Math.floor(NUMBER_OF_YEARS_SHOWN / 2)} +
+ )} +
+ + +
+ ); + } + + private renderBody() { + switch (this.selectionMode) { + case SelectionMode.DAY: + return ( + + ); + case SelectionMode.MONTH: + return ( + + ); + case SelectionMode.YEAR: + return ( + + ); + } + } + + private renderCustomIcon() { + const icon = hasSlot(this.host, 'custom-icon') ? ( + + ) : ( + today + ); + + return ( + + {icon} + + ); + } + + private renderClearable() { + return ( + this.clearable && ( + + ) + ); + } + + render() { + this.adjustPopupPosition(); + + return ( +
(this.wrapper = el)} class="datepicker__container"> + (this.inputElement = el)} + placeholder={this.placeholder} + readonly={this.readonly} + disabled={this.disabled} + name={this.name} + label={this.label} + required={this.required} + errorText={this.errorText} + errorTextCount={this.errorTextCount} + invalid={this.invalid} + onClick={() => this.openCalendar()} + size={this.size} + class={{ 'input--empty': this.value == null }} + > + {this.renderCustomIcon()} + {this.renderClearable()} + {hasSlot(this.host, 'label') ? ( + + + + ) : null} + {hasSlot(this.host, 'error-text') ? ( + + + + ) : null} + + {this.open && ( +
(this.popup = el)} + class={{ + datepicker__popup: true, + 'datepicker__popup--is-up': this.placement != null ? this.placement === 'top' : this.isDropDownContentUp, + 'datepicker__popup--is-inline': this.inline, + }} + > + {this.renderHeader()} + {this.renderBody()} + {this.type === 'date-time' && ( + this.onTimepickerChange(event)} + value={ + this.selectedDate?.getHours() + + ':' + + this.selectedDate?.getMinutes() + + ':' + + this.selectedDate?.getSeconds() + } + > + )} + +
+ )} +
+ ); + } + + private adjustPopupPosition() { + // TODO + // adjustPopupForHoisting( + // this.hoist, + // this.popup, + // this.inputElement, + // this.wrapper, + // MIN_POPUP_HEIGHT, + // (isUp) => (this.isDropDownContentUp = isUp) + // ); + // adjustPopupForSmallScreens(this.popup); + } + + connectedCallback() { + this.eventListeners.forward('six-datepicker-select', 'change', this.host); + this.eventListeners.forward('six-datepicker-blur', 'blur', this.host); + } + + disconnectedCallback() { + this.eventListeners.removeAll(); + } + + private static getCurrentDateAsPointer(): PointerDate { + return { + year: year(now()), + month: month(now()), + day: day(now()), + hours: hours(now()), + minutes: minutes(now()), + seconds: seconds(now()), + }; + } +} diff --git a/libraries/ui-library/src/components/six-date/test/six-datepicker.spec.tsx b/libraries/ui-library/src/components/six-date/test/six-datepicker.spec.tsx new file mode 100644 index 000000000..4b4b478dc --- /dev/null +++ b/libraries/ui-library/src/components/six-date/test/six-datepicker.spec.tsx @@ -0,0 +1,26 @@ +import { newSpecPage } from '@stencil/core/testing'; +import { SixDate } from '../six-date'; + +describe('six-datepicker', () => { + it('renders', async () => { + const page = await newSpecPage({ + components: [SixDate], + html: ``, + }); + expect(page.root).toEqualHtml(` + + +
+ + + + today + + + +
+
+
+ `); + }); +}); diff --git a/libraries/ui-library/src/components/six-date/test/six-datepicker.test-helpers.ts b/libraries/ui-library/src/components/six-date/test/six-datepicker.test-helpers.ts new file mode 100644 index 000000000..13443e66c --- /dev/null +++ b/libraries/ui-library/src/components/six-date/test/six-datepicker.test-helpers.ts @@ -0,0 +1,16 @@ +export const getExpectedMonthString = () => { + return [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ][new Date().getMonth()]; +}; From 69f4ef7e0c87857e4752cc68216cf28f8f94362f Mon Sep 17 00:00:00 2001 From: tkgjl Date: Tue, 16 Apr 2024 10:29:58 +0200 Subject: [PATCH 02/12] changed six-datepicker to six-date in the example --- .../src/components/six-date/index.html | 101 +++++++++--------- 1 file changed, 48 insertions(+), 53 deletions(-) diff --git a/libraries/ui-library/src/components/six-date/index.html b/libraries/ui-library/src/components/six-date/index.html index 5780c64fb..57382d549 100644 --- a/libraries/ui-library/src/components/six-date/index.html +++ b/libraries/ui-library/src/components/six-date/index.html @@ -12,7 +12,7 @@

Datepickers allow the user to manually enter a date or open a popup calendar to select a date.

- +

Examples

@@ -24,7 +24,7 @@

Reading selected date

below the datepicker.

- +
No Date selected yet!
@@ -55,12 +56,12 @@

Date and Time

const datepicker = document.getElementById('read-select-datetime-picker'); const selectedDate = document.getElementById('read-select-datetime-label'); - datepicker.addEventListener('six-datepicker-select', (event) => { + datepicker.addEventListener('six-date-select', (event) => { selectedDate.innerHTML = `selected: ${event.detail.toLocaleString()}`; }); @@ -70,7 +71,7 @@

Disabled State

@@ -81,7 +82,7 @@

Inline State

@@ -95,7 +96,7 @@

Readonly State

@@ -109,7 +110,7 @@

Placement

@@ -120,7 +121,7 @@

Placeholder

@@ -131,7 +132,7 @@

Locale

@@ -144,7 +145,7 @@

Footer

Choose your birthdate!

@@ -168,8 +169,8 @@

Footer

@@ -206,7 +207,7 @@

Date Format

@@ -253,7 +254,7 @@

Define Min & Max

flex-wrap: wrap; } - .min-max-demo six-datepicker { + .min-max-demo six-date { min-width: 15rem; margin-right: 1rem; margin-bottom: 1rem; @@ -275,7 +276,7 @@

Define Min & Max

minDatepicker.min = getDateFromNow(-3); maxDatepicker.max = getDateFromNow(2); - minMaxDatepicker.addEventListener('six-datepicker-select', (event) => { + minMaxDatepicker.addEventListener('six-date-select', (event) => { selectedDate.innerHTML = `${event.detail.toLocaleDateString()}`; }); @@ -296,7 +297,7 @@

Allowed Dates

datepicker.allowedDates = (date) => date.getDate() % 2 === 0; @@ -309,7 +310,7 @@

Custom Start Date

@@ -321,7 +322,7 @@

Clearable

@@ -344,7 +345,7 @@

Icon position

/> @@ -363,7 +364,7 @@

Custom icon

/> @@ -378,7 +379,7 @@

Sizes


@@ -397,7 +398,7 @@

Hoisting

@@ -449,7 +450,7 @@

Open State

@@ -497,7 +498,7 @@

Datepicker in a Modal

box-shadow: 0 4px 16px #00000038; } - .dt-modal six-datepicker { + .dt-modal six-date { width: 100%; height: calc(100vh - 2.5rem); max-width: unset; diff --git a/libraries/ui-library/src/components/six-date/six-date.scss b/libraries/ui-library/src/components/six-date/six-date.scss index c35263d1e..579458ea5 100644 --- a/libraries/ui-library/src/components/six-date/six-date.scss +++ b/libraries/ui-library/src/components/six-date/six-date.scss @@ -10,17 +10,19 @@ position: relative; } - &__popup { - min-width: 400px; - background-color: white; - padding: 0.5em 0.5em 1.5em; - box-shadow: 0 2px 4px -1px rgb(0 0 0 / 20%), 0 4px 5px 0 rgb(0 0 0 / 14%), 0 1px 10px 0 rgb(0 0 0 / 12%); + &__positioner { user-select: none; position: absolute; z-index: var(--six-z-index-dropdown); right: 0; left: 0; + &.popover-visible .datepicker__panel { + opacity: 1; + transform: none; + pointer-events: all; + } + &--is-up { bottom: 100%; // fix problem where you would need to jump around with the cursor when switching months and @@ -34,6 +36,17 @@ border: 1px solid var(--six-color-web-rock-400); } } + + &__panel { + opacity: 0; + pointer-events: none; + transform: scale(0.9); + transition: var(--six-transition-fast) opacity, var(--six-transition-fast) transform; + background-color: white; + min-width: 400px; + padding: 0.5em 0.5em 1.5em; + box-shadow: 0 2px 4px -1px rgb(0 0 0 / 20%), 0 4px 5px 0 rgb(0 0 0 / 14%), 0 1px 10px 0 rgb(0 0 0 / 12%); + } } .datepicker-header { @@ -90,7 +103,7 @@ height: 2.5rem; cursor: pointer; - &:hover { + &:hover:not(&--is-selected) { background-color: var(--six-color-web-rock-100); } diff --git a/libraries/ui-library/src/components/six-date/six-date.tsx b/libraries/ui-library/src/components/six-date/six-date.tsx index 7c5802a18..322a69ab4 100644 --- a/libraries/ui-library/src/components/six-date/six-date.tsx +++ b/libraries/ui-library/src/components/six-date/six-date.tsx @@ -33,7 +33,7 @@ const NUMBER_OF_YEARS_SHOWN = 25; const MIN_POPUP_HEIGHT = 400; -export type SixDatepickerSelectPayload = Date | undefined | null; +export type SixDateSelectPayload = Date | undefined | null; export interface CalendarCell { date: Date; @@ -67,7 +67,8 @@ enum SelectionMode { export class SixDate { private eventListeners = new EventListeners(); private inputElement?: HTMLSixInputElement; - private popup?: HTMLElement; + private positioner?: HTMLElement; + private panel?: HTMLElement; private wrapper?: HTMLElement; private selectedDate?: Date; private popover?: Popover; @@ -81,7 +82,7 @@ export class SixDate { /** * Set the type. */ - @Prop() type: 'date' | 'date-time' = 'date'; + @Prop() type: 'date' | 'time' | 'date-time' = 'date'; /** * The language used to render the weekdays and months. @@ -223,20 +224,31 @@ export class SixDate { this.updatePointerDates(); } + @Watch('open') + protected openChanges() { + console.log('open changes to ' + this.open); + if (!this.popover) return; + if (this.open) { + this.popover.show(); + } else { + this.popover.hide(); + } + } + /** * Emitted when a option got selected. */ - @Event({ eventName: 'six-datepicker-select' }) sixSelect!: EventEmitter; + @Event({ eventName: 'six-date-select' }) sixSelect!: EventEmitter; /** * Emitted when the clear button is activated. */ - @Event({ eventName: 'six-datepicker-clear' }) sixClear!: EventEmitter; + @Event({ eventName: 'six-date-clear' }) sixClear!: EventEmitter; /** * Emitted when a option got selected. */ - @Event({ eventName: 'six-datepicker-blur' }) sixBlur!: EventEmitter; + @Event({ eventName: 'six-date-blur' }) sixBlur!: EventEmitter; @Listen('resize', { target: 'window' }) async resizeHandler() { @@ -576,7 +588,6 @@ export class SixDate { this.selectedDate = this.value; this.updatePointerDates(); this.updateValue(this.value); - this.popover = new Popover(this.host, this.popup!, {}); if (this.inline) { this.open = true; @@ -588,6 +599,10 @@ export class SixDate { } componentDidLoad() { + this.popover = new Popover(this.host, this.positioner!, { + transitionElement: this.panel, + }); + if (this.inputElement != null) { this.eventListeners.add(this.inputElement, 'six-input-input', debounce(this.handleInputChange, this.debounce)); this.eventListeners.add(this.inputElement, 'six-input-blur', this.handleOnBlur); @@ -598,6 +613,24 @@ export class SixDate { this.adjustPopupPosition(); } + private renderElements() { + const isDate = this.type == 'date' || this.type == 'date-time'; + const isTime = this.type == 'time' || this.type == 'date-time'; + + const elements = []; + + if (isDate) { + elements.push(this.renderHeader()); + elements.push(this.renderBody()); + } + + if (isTime) { + elements.push(this.renderTimepicker()); + } + + return elements; + } + private renderHeader() { return (
@@ -676,6 +709,18 @@ export class SixDate { } } + renderTimepicker() { + return ( + this.onTimepickerChange(event)} + value={ + this.selectedDate?.getHours() + ':' + this.selectedDate?.getMinutes() + ':' + this.selectedDate?.getSeconds() + } + > + ); + } + private renderCustomIcon() { const icon = hasSlot(this.host, 'custom-icon') ? ( @@ -752,36 +797,25 @@ export class SixDate { ) : null} - {this.open && ( -
(this.popup = el)} - class={{ - datepicker__popup: true, - 'datepicker__popup--is-up': this.placement != null ? this.placement === 'top' : this.isDropDownContentUp, - 'datepicker__popup--is-inline': this.inline, - }} - > - {this.renderHeader()} - {this.renderBody()} - {this.type === 'date-time' && ( - this.onTimepickerChange(event)} - value={ - this.selectedDate?.getHours() + - ':' + - this.selectedDate?.getMinutes() + - ':' + - this.selectedDate?.getSeconds() - } - > - )} - + {/*{this.open && (*/} +
(this.positioner = el)} + class={{ + datepicker__positioner: true, + 'datepicker__positioner--is-up': + this.placement != null ? this.placement === 'top' : this.isDropDownContentUp, + 'datepicker__positioner--is-inline': this.inline, + }} + > +
(this.panel = el)}> + {this.renderElements()} +
+ - )} +
+ {/*)}*/}
); } @@ -800,8 +834,8 @@ export class SixDate { } connectedCallback() { - this.eventListeners.forward('six-datepicker-select', 'change', this.host); - this.eventListeners.forward('six-datepicker-blur', 'blur', this.host); + this.eventListeners.forward('six-date-select', 'change', this.host); + this.eventListeners.forward('six-date-blur', 'blur', this.host); } disconnectedCallback() { diff --git a/libraries/ui-library/src/components/six-date/test/six-datepicker.spec.tsx b/libraries/ui-library/src/components/six-date/test/six-date.spec.tsx similarity index 82% rename from libraries/ui-library/src/components/six-date/test/six-datepicker.spec.tsx rename to libraries/ui-library/src/components/six-date/test/six-date.spec.tsx index 4b4b478dc..a91284f89 100644 --- a/libraries/ui-library/src/components/six-date/test/six-datepicker.spec.tsx +++ b/libraries/ui-library/src/components/six-date/test/six-date.spec.tsx @@ -5,12 +5,12 @@ describe('six-datepicker', () => { it('renders', async () => { const page = await newSpecPage({ components: [SixDate], - html: ``, + html: ``, }); expect(page.root).toEqualHtml(` - + -
+
@@ -20,7 +20,7 @@ describe('six-datepicker', () => {
- + `); }); }); diff --git a/libraries/ui-library/src/components/six-date/test/six-datepicker.test-helpers.ts b/libraries/ui-library/src/components/six-date/test/six-date.test-helpers.ts similarity index 100% rename from libraries/ui-library/src/components/six-date/test/six-datepicker.test-helpers.ts rename to libraries/ui-library/src/components/six-date/test/six-date.test-helpers.ts From 7063825a083cef0347bb683fef2ab20fc78c3513 Mon Sep 17 00:00:00 2001 From: davidemarcoli Date: Wed, 17 Apr 2024 15:05:50 +0200 Subject: [PATCH 04/12] Fix placement of six-date when there isn't enough space Fix six-date hosting --- .../src/components/six-date/six-date.tsx | 20 +++++++++---------- .../ui-library/src/utils/popover-util.ts | 11 ++++++++++ 2 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 libraries/ui-library/src/utils/popover-util.ts diff --git a/libraries/ui-library/src/components/six-date/six-date.tsx b/libraries/ui-library/src/components/six-date/six-date.tsx index 322a69ab4..2a39df303 100644 --- a/libraries/ui-library/src/components/six-date/six-date.tsx +++ b/libraries/ui-library/src/components/six-date/six-date.tsx @@ -28,6 +28,7 @@ import { DaySelection } from './components/day-selection'; import { YearSelection } from './components/year-selection'; import { SixTimepickerChange } from '../six-timepicker/six-timepicker'; import Popover from '../../utils/popover'; +import { calcIsDropDownContentUp } from '../../utils/popover-util'; const NUMBER_OF_YEARS_SHOWN = 25; @@ -226,7 +227,6 @@ export class SixDate { @Watch('open') protected openChanges() { - console.log('open changes to ' + this.open); if (!this.popover) return; if (this.open) { this.popover.show(); @@ -265,8 +265,11 @@ export class SixDate { private moveOpenHoistedPopup() { this.popover?.reposition(); - // TODO - // movePopup(this.hoist, this.open, this.popup, this.inputElement, this.wrapper, MIN_POPUP_HEIGHT); + this.popover?.setOptions({ + placement: calcIsDropDownContentUp(this.inputElement!, this.wrapper!, MIN_POPUP_HEIGHT) + ? 'top-start' + : 'bottom-start', + }); } get container() { @@ -821,15 +824,10 @@ export class SixDate { } private adjustPopupPosition() { + this.popover?.setOptions({ + strategy: this.hoist ? 'fixed' : 'absolute', + }); // TODO - // adjustPopupForHoisting( - // this.hoist, - // this.popup, - // this.inputElement, - // this.wrapper, - // MIN_POPUP_HEIGHT, - // (isUp) => (this.isDropDownContentUp = isUp) - // ); // adjustPopupForSmallScreens(this.popup); } diff --git a/libraries/ui-library/src/utils/popover-util.ts b/libraries/ui-library/src/utils/popover-util.ts new file mode 100644 index 000000000..59b0bf410 --- /dev/null +++ b/libraries/ui-library/src/utils/popover-util.ts @@ -0,0 +1,11 @@ +export function calcIsDropDownContentUp( + inputElement: HTMLElement, + wrapper: HTMLElement, + minPopupHeight: number +): boolean { + const inputBoundingRect = inputElement.getBoundingClientRect(); + const wrapperBoundingRect = wrapper.getBoundingClientRect(); + const popupHeight = Math.max(wrapperBoundingRect.height, minPopupHeight); + const moreSpaceInTop = inputBoundingRect.y > window.innerHeight / 2; + return moreSpaceInTop && window.innerHeight < inputBoundingRect.bottom + popupHeight; +} From 56edb80ad7aef12e8e9ca154275f4d8d170a00d3 Mon Sep 17 00:00:00 2001 From: davidemarcoli Date: Wed, 17 Apr 2024 15:25:07 +0200 Subject: [PATCH 05/12] Fix placement of six-date wrong when there is a label or help-text --- .../src/components/six-date/six-date.scss | 7 ----- .../src/components/six-date/six-date.tsx | 26 +++++++++++++------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/libraries/ui-library/src/components/six-date/six-date.scss b/libraries/ui-library/src/components/six-date/six-date.scss index 579458ea5..ac6f24989 100644 --- a/libraries/ui-library/src/components/six-date/six-date.scss +++ b/libraries/ui-library/src/components/six-date/six-date.scss @@ -23,13 +23,6 @@ pointer-events: all; } - &--is-up { - bottom: 100%; - // fix problem where you would need to jump around with the cursor when switching months and - // its a dropup since not all months have the same height - min-height: 382px; - } - &--is-inline { position: initial; box-shadow: none; diff --git a/libraries/ui-library/src/components/six-date/six-date.tsx b/libraries/ui-library/src/components/six-date/six-date.tsx index 2a39df303..434e57bb4 100644 --- a/libraries/ui-library/src/components/six-date/six-date.tsx +++ b/libraries/ui-library/src/components/six-date/six-date.tsx @@ -266,9 +266,7 @@ export class SixDate { private moveOpenHoistedPopup() { this.popover?.reposition(); this.popover?.setOptions({ - placement: calcIsDropDownContentUp(this.inputElement!, this.wrapper!, MIN_POPUP_HEIGHT) - ? 'top-start' - : 'bottom-start', + placement: this.isDropDownContentUp ? 'top-start' : 'bottom-start', }); } @@ -304,8 +302,15 @@ export class SixDate { if (this.inputElement == null || this.wrapper == null) { return; } - // TODO - //this.isDropDownContentUp = calcIsDropDownContentUp(this.inputElement, this.wrapper, MIN_POPUP_HEIGHT); + + // TODO: check if this is needed (it gets the input without label and help-text + if (this.inputElement.shadowRoot?.children[1]) { + this.isDropDownContentUp = calcIsDropDownContentUp( + this.inputElement!.shadowRoot!.children[1]!.children[1] as HTMLElement, + this.wrapper, + MIN_POPUP_HEIGHT + ); + } } private getMonthStringForIndex(index: number) { @@ -602,9 +607,14 @@ export class SixDate { } componentDidLoad() { - this.popover = new Popover(this.host, this.positioner!, { - transitionElement: this.panel, - }); + this.popover = new Popover( + // TODO: check if this is correct like that (it gets the input without label and help-text + this.inputElement!.shadowRoot!.children[1]!.children[1]! as HTMLElement, + this.positioner!, + { + transitionElement: this.panel, + } + ); if (this.inputElement != null) { this.eventListeners.add(this.inputElement, 'six-input-input', debounce(this.handleInputChange, this.debounce)); From 35492c87642be6981af7b910e92bc8e2afed4121 Mon Sep 17 00:00:00 2001 From: tkgjl Date: Mon, 3 Jun 2024 16:45:18 +0200 Subject: [PATCH 06/12] Update SixHeader component with new slots and events --- libraries/ui-library/src/components/six-header/six-header.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/ui-library/src/components/six-header/six-header.tsx b/libraries/ui-library/src/components/six-header/six-header.tsx index cc5961864..e67b986f6 100644 --- a/libraries/ui-library/src/components/six-header/six-header.tsx +++ b/libraries/ui-library/src/components/six-header/six-header.tsx @@ -319,7 +319,8 @@ export class SixHeader { return ( -
+
+ {hamburgerMenu} {logo} From d4ab819aeabd005912c39a3e9205fdb557ebc674 Mon Sep 17 00:00:00 2001 From: tkgjl Date: Tue, 4 Jun 2024 11:04:14 +0200 Subject: [PATCH 07/12] Update SixHeader-tests according to the changes made in six.header.tsx --- .../src/components/six-header/test/six-header.spec.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/ui-library/src/components/six-header/test/six-header.spec.tsx b/libraries/ui-library/src/components/six-header/test/six-header.spec.tsx index 985368cfc..f5a9c0852 100644 --- a/libraries/ui-library/src/components/six-header/test/six-header.spec.tsx +++ b/libraries/ui-library/src/components/six-header/test/six-header.spec.tsx @@ -19,7 +19,7 @@ describe('six-header', () => { expect(page.root).toEqualHtml(` -
+
@@ -63,7 +63,7 @@ describe('six-header', () => { expect(page.root).toEqualHtml(` -
+
@@ -105,7 +105,7 @@ describe('six-header', () => { expect(page.root).toEqualHtml(` -
+
@@ -157,7 +157,7 @@ describe('six-header', () => { expect(page.root).toEqualHtml(` -
+