-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: implement basic OnyxDatePicker
component
#2145
Open
larsrickert
wants to merge
24
commits into
main
Choose a base branch
from
feat/1818-datepicker
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
ad4681c
feat: implement basic datepicker
larsrickert 87fa885
add min and max property
larsrickert 3f2d65d
rename component
larsrickert 4e9427d
remove min and max for now
larsrickert d187362
remove unused arg types
larsrickert 9e715e2
remove usage of OnyxInput
larsrickert 6be65cc
add basic screenshot tests
larsrickert 0ee557c
fix input fill
larsrickert 8c55060
fix error message tests
larsrickert e0da3a7
try to fix tests
larsrickert b2dbfec
update tests
larsrickert 08b5461
docs(changeset): feat: implement basic `OnyxDatePicker` component
larsrickert 3fc4037
remove autocomplete
larsrickert 73a98da
update selection colors
larsrickert 553a396
update open test
larsrickert c69cb81
limit viewport size for open screenshots
larsrickert 5ef2885
remove custom selection colors
larsrickert f737d24
try to fix open screenshots
larsrickert 7ced4b4
update screenshot tests
larsrickert 8944d09
fix screenshot width
larsrickert bb7f611
revert selection change
larsrickert 95d0cfd
chore: update Playwright screenshots (#2161)
github-actions[bot] 7bd6616
move date util to dedicated file
JoCa96 d2e9e32
mini refactor
JoCa96 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"sit-onyx": minor | ||
--- | ||
|
||
feat: implement basic `OnyxDatePicker` component |
Binary file added
BIN
+25.6 KB
...snapshots/components/OnyxDatePicker/DatePicker-date-default--chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+40 KB
.../snapshots/components/OnyxDatePicker/DatePicker-date-default--firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+26.8 KB
...t/snapshots/components/OnyxDatePicker/DatePicker-date-default--webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+27.1 KB
...pshots/components/OnyxDatePicker/DatePicker-date-with-value--chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+40.3 KB
...apshots/components/OnyxDatePicker/DatePicker-date-with-value--firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+33.2 KB
...napshots/components/OnyxDatePicker/DatePicker-date-with-value--webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+27.1 KB
...components/OnyxDatePicker/DatePicker-datetime-local-default--chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+41.4 KB
.../components/OnyxDatePicker/DatePicker-datetime-local-default--firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+27.9 KB
...s/components/OnyxDatePicker/DatePicker-datetime-local-default--webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+30.5 KB
...ponents/OnyxDatePicker/DatePicker-datetime-local-with-value--chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+44.4 KB
...mponents/OnyxDatePicker/DatePicker-datetime-local-with-value--firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+38.9 KB
...omponents/OnyxDatePicker/DatePicker-datetime-local-with-value--webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions
66
packages/sit-onyx/src/components/OnyxDatePicker/OnyxDatePicker.ct.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { DENSITIES } from "../../composables/density"; | ||
import { expect, test } from "../../playwright/a11y"; | ||
import { executeMatrixScreenshotTest } from "../../playwright/screenshots"; | ||
import OnyxDatePicker from "./OnyxDatePicker.vue"; | ||
|
||
test.describe("Screenshot tests", () => { | ||
for (const type of ["date", "datetime-local"] as const) { | ||
for (const state of ["default", "with value"] as const) { | ||
executeMatrixScreenshotTest({ | ||
name: `DatePicker (${type}, ${state})`, | ||
columns: DENSITIES, | ||
rows: ["default", "hover", "focus"], | ||
component: (column) => { | ||
return ( | ||
<OnyxDatePicker | ||
label="Test label" | ||
density={column} | ||
modelValue={state === "with value" ? new Date(2024, 10, 25, 14, 30) : undefined} | ||
style="width: 16rem;" | ||
type={type} | ||
/> | ||
); | ||
}, | ||
beforeScreenshot: async (component, page, column, row) => { | ||
const datepicker = component.getByLabel("Test label"); | ||
if (row === "hover") await datepicker.hover(); | ||
if (row === "focus") await datepicker.focus(); | ||
}, | ||
}); | ||
} | ||
} | ||
}); | ||
|
||
test("should emit events", async ({ mount, makeAxeBuilder }) => { | ||
const events = { | ||
updateModelValue: [] as (string | undefined)[], | ||
}; | ||
|
||
// ARRANGE | ||
const component = await mount( | ||
<OnyxDatePicker | ||
label="Label" | ||
onUpdate:modelValue={(value) => events.updateModelValue.push(value)} | ||
/>, | ||
); | ||
|
||
// should not emit initial events | ||
expect(events).toMatchObject({ updateModelValue: [] }); | ||
|
||
// ACT | ||
const accessibilityScanResults = await makeAxeBuilder().analyze(); | ||
|
||
// ASSERT | ||
expect(accessibilityScanResults.violations).toEqual([]); | ||
|
||
const inputElement = component.getByLabel("Label"); | ||
|
||
// ACT | ||
await inputElement.fill("2024-11-25"); | ||
|
||
// ASSERT | ||
await expect(inputElement).toHaveValue("2024-11-25"); | ||
expect(events).toMatchObject({ | ||
updateModelValue: ["2024-11-25T00:00:00.000Z"], | ||
}); | ||
}); |
33 changes: 33 additions & 0 deletions
33
packages/sit-onyx/src/components/OnyxDatePicker/OnyxDatePicker.stories.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { withNativeEventLogging } from "@sit-onyx/storybook-utils"; | ||
import type { Meta, StoryObj } from "@storybook/vue3"; | ||
import OnyxDatePicker from "./OnyxDatePicker.vue"; | ||
|
||
const meta: Meta<typeof OnyxDatePicker> = { | ||
title: "Form Elements/DatePicker", | ||
component: OnyxDatePicker, | ||
decorators: [ | ||
(story) => ({ | ||
components: { story }, | ||
template: `<div style="width: 16rem;"> <story /> </div>`, | ||
}), | ||
], | ||
argTypes: { | ||
...withNativeEventLogging(["onInput", "onChange", "onFocusin", "onFocusout"]), | ||
}, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof OnyxDatePicker>; | ||
|
||
export const Date = { | ||
args: { | ||
label: "Date", | ||
}, | ||
} satisfies Story; | ||
|
||
export const Datetime = { | ||
args: { | ||
label: "Date + time", | ||
type: "datetime-local", | ||
}, | ||
} satisfies Story; |
150 changes: 150 additions & 0 deletions
150
packages/sit-onyx/src/components/OnyxDatePicker/OnyxDatePicker.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
<script lang="ts" setup> | ||
import { computed } from "vue"; | ||
import { useDensity } from "../../composables/density"; | ||
import { getFormMessages, useCustomValidity } from "../../composables/useCustomValidity"; | ||
import { useErrorClass } from "../../composables/useErrorClass"; | ||
import { SKELETON_INJECTED_SYMBOL, useSkeletonContext } from "../../composables/useSkeletonState"; | ||
import { isValidDate } from "../../utils/date"; | ||
import { FORM_INJECTED_SYMBOL, useFormContext } from "../OnyxForm/OnyxForm.core"; | ||
import OnyxFormElement from "../OnyxFormElement/OnyxFormElement.vue"; | ||
import OnyxLoadingIndicator from "../OnyxLoadingIndicator/OnyxLoadingIndicator.vue"; | ||
import OnyxSkeleton from "../OnyxSkeleton/OnyxSkeleton.vue"; | ||
import type { DateValue, OnyxDatePickerProps } from "./types"; | ||
|
||
const props = withDefaults(defineProps<OnyxDatePickerProps>(), { | ||
type: "date", | ||
required: false, | ||
readonly: false, | ||
loading: false, | ||
skeleton: SKELETON_INJECTED_SYMBOL, | ||
disabled: FORM_INJECTED_SYMBOL, | ||
showError: FORM_INJECTED_SYMBOL, | ||
}); | ||
|
||
const emit = defineEmits<{ | ||
/** | ||
* Emitted when the current value changes. Will be a ISO timestamp created by `new Date().toISOString()`. | ||
*/ | ||
"update:modelValue": [value?: string]; | ||
/** | ||
* Emitted when the validity state of the input changes. | ||
*/ | ||
validityChange: [validity: ValidityState]; | ||
}>(); | ||
|
||
const { vCustomValidity, errorMessages } = useCustomValidity({ props, emit }); | ||
const successMessages = computed(() => getFormMessages(props.success)); | ||
const messages = computed(() => getFormMessages(props.message)); | ||
const { densityClass } = useDensity(props); | ||
const { disabled, showError } = useFormContext(props); | ||
const skeleton = useSkeletonContext(props); | ||
const errorClass = useErrorClass(showError); | ||
|
||
/** | ||
* Gets the normalized date based on the input type that can be passed to the native HTML `<input />`. | ||
* Will be checked to be a valid date. | ||
* | ||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Date_and_time_formats#date_strings | ||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Date_and_time_formats#local_date_and_time_strings | ||
*/ | ||
const getNormalizedDate = computed(() => { | ||
return (value?: DateValue) => { | ||
const date = value != undefined ? new Date(value) : undefined; | ||
if (!isValidDate(date)) return; | ||
|
||
const dateString = date.toISOString().split("T")[0]; | ||
if (props.type === "date") return dateString; | ||
|
||
// for datetime type, the hour must be in the users local timezone so just returning the string returned by `toISOString()` will be invalid | ||
// since the timezone offset is missing then | ||
const hours = date.getHours().toString().padStart(2, "0"); | ||
const minutes = date.getMinutes().toString().padStart(2, "0"); | ||
return `${dateString}T${hours}:${minutes}`; | ||
}; | ||
}); | ||
|
||
/** | ||
* Current value (with getter and setter) that can be used as "v-model" for the native input. | ||
*/ | ||
const value = computed({ | ||
get: () => getNormalizedDate.value(props.modelValue), | ||
set: (value) => { | ||
const newDate = new Date(value ?? ""); | ||
emit("update:modelValue", isValidDate(newDate) ? newDate.toISOString() : undefined); | ||
}, | ||
}); | ||
</script> | ||
|
||
<template> | ||
<div v-if="skeleton" :class="['onyx-datepicker-skeleton', densityClass]"> | ||
<OnyxSkeleton v-if="!props.hideLabel" class="onyx-datepicker-skeleton__label" /> | ||
<OnyxSkeleton class="onyx-datepicker-skeleton__input" /> | ||
</div> | ||
|
||
<div v-else :class="['onyx-datepicker', densityClass, errorClass]"> | ||
<OnyxFormElement | ||
v-bind="props" | ||
:error-messages="errorMessages" | ||
:success-messages="successMessages" | ||
:message="messages" | ||
> | ||
<template #default="{ id: inputId }"> | ||
<div class="onyx-datepicker__wrapper"> | ||
<OnyxLoadingIndicator | ||
v-if="props.loading" | ||
class="onyx-datepicker__loading" | ||
type="circle" | ||
/> | ||
<!-- key is needed to keep current value when switching between date and datetime type --> | ||
<input | ||
:id="inputId" | ||
:key="props.type" | ||
v-model="value" | ||
v-custom-validity | ||
class="onyx-datepicker__native" | ||
:class="{ 'onyx-datepicker__native--success': successMessages }" | ||
:type="props.type" | ||
:required="props.required" | ||
:autofocus="props.autofocus" | ||
:name="props.name" | ||
:readonly="props.readonly" | ||
:disabled="disabled || props.loading" | ||
:aria-label="props.hideLabel ? props.label : undefined" | ||
:title="props.hideLabel ? props.label : undefined" | ||
/> | ||
</div> | ||
</template> | ||
</OnyxFormElement> | ||
</div> | ||
</template> | ||
|
||
<style lang="scss"> | ||
@use "../../styles/mixins/layers.scss"; | ||
@use "../../styles/mixins/input.scss"; | ||
|
||
.onyx-datepicker, | ||
.onyx-datepicker-skeleton { | ||
--onyx-datepicker-padding-vertical: var(--onyx-density-xs); | ||
} | ||
|
||
.onyx-datepicker-skeleton { | ||
@include input.define-skeleton-styles( | ||
$height: calc(1lh + 2 * var(--onyx-datepicker-padding-vertical)) | ||
); | ||
} | ||
|
||
.onyx-datepicker { | ||
@include layers.component() { | ||
@include input.define-shared-styles( | ||
$base-selector: ".onyx-datepicker", | ||
$vertical-padding: var(--onyx-datepicker-padding-vertical) | ||
); | ||
|
||
&__native { | ||
&::-webkit-calendar-picker-indicator { | ||
cursor: pointer; | ||
} | ||
} | ||
} | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import type { OnyxInputProps } from "../OnyxInput/types"; | ||
|
||
export type OnyxDatePickerProps = Omit< | ||
OnyxInputProps, | ||
| "type" | ||
| "modelValue" | ||
| "autocapitalize" | ||
| "maxlength" | ||
| "minlength" | ||
| "pattern" | ||
| "withCounter" | ||
| "placeholder" | ||
| "autocomplete" | ||
> & { | ||
/** | ||
* Current date value. Supports all data types that are parsable by `new Date()`. | ||
*/ | ||
modelValue?: DateValue; | ||
/** | ||
* Whether the user should be able to select only date or date + time. | ||
*/ | ||
type?: "date" | "datetime-local"; | ||
}; | ||
|
||
/** Data types that are parsable as date via `new Date()`. */ | ||
export type DateValue = ConstructorParameters<typeof Date>[0]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { describe, expect, test } from "vitest"; | ||
import { isValidDate } from "./date"; | ||
|
||
describe("date", () => { | ||
test.each([ | ||
{ input: "", isValid: false }, | ||
{ input: 0, isValid: false }, | ||
{ input: false, isValid: false }, | ||
{ input: undefined, isValid: false }, | ||
{ input: null, isValid: false }, | ||
{ input: "not-a-date", isValid: false }, | ||
{ input: new Date("not-a-date"), isValid: false }, | ||
{ input: new Date(), isValid: true }, | ||
])("should determine correctly if $input is a valid date", ({ input, isValid }) => { | ||
expect(isValidDate(input)).toBe(isValid); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/** | ||
* Checks whether the given value is a valid `Date` object. | ||
* | ||
* @example isValidDate(new Date()) // true | ||
* @example isValidDate("not-a-date") // false | ||
*/ | ||
export const isValidDate = (date: unknown): date is Date => { | ||
// isNaN supports Date objects so the type cast here is safe | ||
return date instanceof Date && !isNaN(date as unknown as number); | ||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a particular reason why we don't use
valueAsDate
? Then we don't need to do the conversion magic ourselves 😅