mirror of
https://github.com/navikt/aksel.git
synced 2026-10-16 10:42:03 +00:00
Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a4465a051 | ||
|
|
b77dbe0cad | ||
|
|
78fc16c1a9 | ||
|
|
5937d085c0 | ||
|
|
2f248a65f7 | ||
|
|
08e134ef3c | ||
|
|
821343b67d | ||
|
|
19b42daa69 | ||
|
|
09c44edccb | ||
|
|
2fab45f757 | ||
|
|
1ba5d9b5a2 | ||
|
|
5a749690c6 | ||
|
|
2f7873d3e7 | ||
|
|
e5dc975c44 | ||
|
|
1cc24c2998 | ||
|
|
636eb05bf5 | ||
|
|
c6424eda70 | ||
|
|
90f76498d6 | ||
|
|
6201d87a1a | ||
|
|
4dcb4efeea | ||
|
|
5586d42096 | ||
|
|
94e3bd7191 | ||
|
|
0d886b12bf | ||
|
|
dfbec997fc | ||
|
|
1ec34524e7 | ||
|
|
36f84458a5 | ||
|
|
2271072d1c | ||
|
|
0c22de8c61 | ||
|
|
1b20ad54de | ||
|
|
8576400263 |
No files matched your search
@@ -4,14 +4,6 @@ import { Markdown, Meta } from "@storybook/blocks";
|
||||
|
||||
# Migration
|
||||
|
||||
## Missing features
|
||||
|
||||
Some components are not yet implemented in the new system. These are:
|
||||
|
||||
- ConfirmationPanel
|
||||
- Panel
|
||||
- Dropdown
|
||||
|
||||
## Tokens
|
||||
|
||||
[See tokens docs here](https://main--66b4b3beb91603ed0ab5c45e.chromatic.com/?path=/docs/docs-become-a-pilot-team-4-tokens--docs).
|
||||
@@ -40,6 +32,15 @@ All component-spesific tokens are no longer supported. That means all tokens lik
|
||||
|
||||
All component-based tokens are prefixed with `--ac`, so a global search in your codebase should find all of them.
|
||||
|
||||
## Tailwind CSS
|
||||
|
||||
Since our Tailwind CSS config is tied to our tokens, most changes made to the tokens will affect the Tailwind CSS config.
|
||||
We have decided to prefix all our classes with `ax-` in the new config, so it should be possible to use both packages at the same time. We will develop tooling to hopefuly make the migrations easier in the future.
|
||||
|
||||
### Removed classes
|
||||
|
||||
z-index, maxWidth and spacing classes are removed from the new config.
|
||||
|
||||
## React
|
||||
|
||||
We have made some updates to `@navikt/ds-react` that you should be aware of before testing.
|
||||
@@ -64,9 +65,9 @@ custom button.
|
||||
|
||||
Component got a complete overhaul. Custom overrides might break.
|
||||
|
||||
### Popover, HelpText, Tooltip
|
||||
### Popover, HelpText
|
||||
|
||||
`arrow`-prop is removed. All of our floating dialog-elements now come without arrow.
|
||||
`arrow`-prop is removed. All of our floating dialog-elements except for `Tooltip` now comes without arrow.
|
||||
If you have custom `offset`-values, you might have to update them to accommodate this.
|
||||
|
||||
### Pagination
|
||||
|
||||
@@ -6,18 +6,18 @@ import { Markdown, Meta } from "@storybook/blocks";
|
||||
|
||||
## Clean project
|
||||
|
||||
For new projects, setup is just as easy as before, but now requires `Theme`-component to be wrapped around your app.
|
||||
For new projects, setup is just as easy as before, but now requires a `Theme`-component to be wrapped around your app.
|
||||
The component acts as a local feature-flag for the new implementation, with the added benefit of being able to switch between light and dark mode for you.
|
||||
|
||||
Steps:
|
||||
|
||||
- 1. Import new CSS `@navikt/ds-css/darkside`
|
||||
- 2. Import `Theme`-component from `@navikt/ds-react`
|
||||
- 2. Import `Theme`-component from `@navikt/ds-react/Theme`
|
||||
- 3. Wrap your app in the `Theme`-component. You do not have to set the theme, but it defaults to `light`.
|
||||
|
||||
```tsx
|
||||
import "@navikt/ds-css/darkside";
|
||||
import { Theme } from "@navikt/ds-react";
|
||||
import { Theme } from "@navikt/ds-react/Theme";
|
||||
|
||||
export const App = () => (
|
||||
<Theme theme="light">
|
||||
@@ -32,7 +32,7 @@ If you are not using any of our React-components, you can still switch between t
|
||||
|
||||
## Theme switching
|
||||
|
||||
The `Theme`-component has a `theme`-prop that can be set to either `light` or `dark`. This only sets a `class` on the element of `light` or `dark`, and the rest is handled by the CSS.
|
||||
The `Theme`-component has a `theme`-prop that can be set to either `light` or `dark`. This only sets a `class` on the element of `light` or `dark` (in addition to a `navds-theme` class), and the rest is handled by the CSS.
|
||||
|
||||
You have the option of using `hasBackground` (defaults to true) to make `Theme` include a built on default background.
|
||||
|
||||
@@ -87,7 +87,7 @@ For the new system to work with our components, you will have to use the new `Th
|
||||
|
||||
```tsx
|
||||
import "@navikt/ds-css/darkside;";
|
||||
import { Theme } from "@navikt/ds-react";
|
||||
import { Theme } from "@navikt/ds-react/Theme";
|
||||
|
||||
export const App = () => (
|
||||
<Theme theme="light">
|
||||
@@ -117,3 +117,12 @@ Since our implementation is based on CSS, you can easily make custom adjustments
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tailwind CSS
|
||||
|
||||
```
|
||||
// Tailwind v3
|
||||
module.exports = {
|
||||
presets: [require("@navikt/ds-tailwind/darkside-tw3")],
|
||||
}
|
||||
```
|
||||
@@ -44,6 +44,7 @@ We welcome all forms of feedback:
|
||||
- **Design tokens**: A complete re-write of our design tokens to allow for dark mode.
|
||||
- **Dark mode**: A new dark mode implementation.
|
||||
- **Updated CSS**: Now uses new tokens, built-in CSS-layers support and most components are re-written.
|
||||
- **Updated Tailwind CSS config**: Now uses new tokens, now comes with built-in `ax`-prefix for easier separation between Aksel and your own config.
|
||||
- **Updated components**: Components use the new tokens and CSS.
|
||||
- **New Theme-component**: A new `<Theme/>`-component to handle the theming-switch locally.
|
||||
- **Updated documentation**: Updated documentation for the new implementation.
|
||||
@@ -54,9 +55,14 @@ As we go, we will add more documentation to help you get started with the new im
|
||||
|
||||
- [Design i light og dark-mode](https://aksel.nav.no/god-praksis/artikler/design-i-dark-mode-og-light-mode)
|
||||
|
||||
### Demo page
|
||||
|
||||
We have a demo page to show off some of the new features and tokens. [You can find it here](https://aksel-referansesider.ansatt.dev.nav.no/aktivitetsplan/).
|
||||
We recommend using "inspect-tool" in browser to get a feel for the new tokens and CSS.
|
||||
|
||||
## How testing could affect your application
|
||||
|
||||
All the changes are **opt in**, meaning you can choose to use nothing, some parts of the new implementation, or go all inn.
|
||||
All the changes are **opt in**, meaning you can choose to use nothing, some parts of the new implementation, or go all-in.
|
||||
You will still be able to recieve updates to the old implementation while we are in the testing phase.
|
||||
|
||||
### Tokens
|
||||
@@ -67,7 +73,7 @@ This includes new backgrounds, text-colors, border-colors and updated spacing-to
|
||||
- All tokens have a new prefix `--ax` to avoid conflicts with old tokens.
|
||||
- You will be able to use the new tokens at the same time as the old tokens.
|
||||
- All background, text-colors and border-colors have new names and "concepts" to learn. Migration will not be a quick one to one with old system.
|
||||
- New spacing tokens are now named `space` and based on `px` values.
|
||||
- New spacing tokens are now named `space` and based on `px` values (most devs think and deal in `px` values first, no more on-the-fly mental arithmetic! WYSIWYG).
|
||||
|
||||
### CSS
|
||||
|
||||
@@ -78,6 +84,11 @@ We have updated our CSS to be based on the new tokens for theming.
|
||||
- You will not be able to use the old CSS and the new CSS at the same time.
|
||||
- Component-based tokens are no longer avaliable.
|
||||
|
||||
### Tailwind CSS
|
||||
|
||||
We have made a new Tailwind CSS config based on the new tokens. Based on previous iterations, we have decided to try prefixing all our classes with `ax-`.
|
||||
While this will be a little more verbose, it makes it easier to separate your own config from ours, and hopefully makes it easier to maintain.
|
||||
|
||||
### React components
|
||||
|
||||
We have made some prop-changes to some components to facilitate the new theming and updated CSS. You can read more about them under `migration` in the sidebar.
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @navikt/aksel-icons
|
||||
|
||||
## 7.14.1
|
||||
|
||||
## 7.14.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Icons: :tada: New icons `LineHeight`, `SpaceHorizontal` and `SpaceVertical`. ([#3587](https://github.com/navikt/aksel/pull/3587))
|
||||
|
||||
## 7.13.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Icons: :tada: New icons `ExpandVertical` and `QuestionmarkCircle` ([#3583](https://github.com/navikt/aksel/pull/3583))
|
||||
|
||||
## 7.12.2
|
||||
|
||||
## 7.12.1
|
||||
|
||||
## 7.12.0
|
||||
|
||||
@@ -12,5 +12,5 @@ keywords:
|
||||
- struktur
|
||||
- innhold
|
||||
variant: Stroke
|
||||
updated_at: 05.01.2024
|
||||
updated_at: 10.02.2025
|
||||
created_at: 07.03.2023
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5303 2.21967C12.2374 1.92678 11.7626 1.92678 11.4697 2.21967L7.46967 6.21967C7.17678 6.51256 7.17678 6.98744 7.46967 7.28033C7.76256 7.57322 8.23744 7.57322 8.53033 7.28033L11.25 4.56066L11.25 10C11.25 10.4142 11.5858 10.75 12 10.75C12.4142 10.75 12.75 10.4142 12.75 10L12.75 4.56066L15.4697 7.28033C15.7626 7.57322 16.2374 7.57322 16.5303 7.28033C16.8232 6.98744 16.8232 6.51256 16.5303 6.21967L12.5303 2.21967ZM11.4697 21.7803C11.7626 22.0732 12.2374 22.0732 12.5303 21.7803L16.5303 17.7803C16.8232 17.4874 16.8232 17.0126 16.5303 16.7197C16.2374 16.4268 15.7626 16.4268 15.4697 16.7197L12.75 19.4393L12.75 14C12.75 13.5858 12.4142 13.25 12 13.25C11.5858 13.25 11.25 13.5858 11.25 14L11.25 19.4393L8.53033 16.7197C8.23744 16.4268 7.76256 16.4268 7.46967 16.7197C7.17678 17.0126 7.17678 17.4874 7.46967 17.7803L11.4697 21.7803Z" fill="#23262A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1003 B |
@@ -0,0 +1,13 @@
|
||||
name: ExpandVertical
|
||||
category: Arrows
|
||||
sub_category: Arrow
|
||||
keywords:
|
||||
- arrows
|
||||
- piler
|
||||
- fullscreen
|
||||
- fullskjerm
|
||||
- open
|
||||
- åpne
|
||||
variant: Stroke
|
||||
updated_at: 30.01.2025
|
||||
created_at: 29.01.2025
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 3.25C3.33579 3.25 3 3.58579 3 4C3 4.41421 3.33579 4.75 3.75 4.75H20.25C20.6642 4.75 21 4.41421 21 4C21 3.58579 20.6642 3.25 20.25 3.25H3.75ZM12.6855 6.9454C12.5652 6.67455 12.2966 6.5 12.0002 6.5C11.7038 6.5 11.4352 6.67455 11.3148 6.9454L7.31483 15.9454C7.1466 16.3239 7.31707 16.7671 7.69558 16.9354C8.0741 17.1036 8.51732 16.9331 8.68555 16.5546L9.5987 14.5H14.4017L15.3148 16.5546C15.4831 16.9331 15.9263 17.1036 16.3048 16.9354C16.6833 16.7671 16.8538 16.3239 16.6855 15.9454L12.6855 6.9454ZM13.735 13L12.0002 9.09666L10.2654 13H13.735ZM3 20C3 19.5858 3.33579 19.25 3.75 19.25H20.25C20.6642 19.25 21 19.5858 21 20C21 20.4142 20.6642 20.75 20.25 20.75H3.75C3.33579 20.75 3 20.4142 3 20Z" fill="#23262A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 867 B |
@@ -0,0 +1,9 @@
|
||||
name: LineHeight
|
||||
category: Interface
|
||||
sub_category: Design
|
||||
keywords:
|
||||
- linjehøyde
|
||||
- linjeavstand
|
||||
variant: Stroke
|
||||
updated_at: 14.02.2025
|
||||
created_at: 14.02.2025
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 3.75C7.44365 3.75 3.75 7.44365 3.75 12C3.75 16.5563 7.44365 20.25 12 20.25C16.5563 20.25 20.25 16.5563 20.25 12C20.25 7.44365 16.5563 3.75 12 3.75ZM2.25 12C2.25 6.61522 6.61522 2.25 12 2.25C17.3848 2.25 21.75 6.61522 21.75 12C21.75 17.3848 17.3848 21.75 12 21.75C6.61522 21.75 2.25 17.3848 2.25 12ZM11.0498 16.9C11.0498 16.3753 11.4751 15.95 11.9998 15.95C12.5245 15.95 12.9498 16.3753 12.9498 16.9C12.9498 17.4247 12.5245 17.85 11.9998 17.85C11.4751 17.85 11.0498 17.4247 11.0498 16.9ZM10.25 9.5C10.25 8.5335 11.0335 7.75 12 7.75H12.1361C13.0274 7.75 13.75 8.47255 13.75 9.36387C13.75 9.85413 13.5271 10.3178 13.1443 10.6241L12.7028 10.9773C11.7845 11.7119 11.25 12.8241 11.25 14C11.25 14.4142 11.5858 14.75 12 14.75C12.4142 14.75 12.75 14.4142 12.75 14C12.75 13.2797 13.0774 12.5985 13.6398 12.1486L14.0814 11.7954C14.82 11.2045 15.25 10.3098 15.25 9.36387C15.25 7.64413 13.8559 6.25 12.1361 6.25H12C10.2051 6.25 8.75 7.70507 8.75 9.5C8.75 9.91421 9.08579 10.25 9.5 10.25C9.91421 10.25 10.25 9.91421 10.25 9.5Z" fill="#23262A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,17 @@
|
||||
name: QuestionmarkCircle
|
||||
category: Status
|
||||
sub_category: Guidance
|
||||
keywords:
|
||||
- assistance
|
||||
- support
|
||||
- information
|
||||
- guidance
|
||||
- question mark
|
||||
- hjelp
|
||||
- støtte
|
||||
- informasjon
|
||||
- veiledning
|
||||
- spørsmålstegn
|
||||
variant: Stroke
|
||||
updated_at: 29.01.2025
|
||||
created_at: 29.01.2025
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 21.75C17.3848 21.75 21.75 17.3848 21.75 12C21.75 6.61522 17.3848 2.25 12 2.25C6.61522 2.25 2.25 6.61522 2.25 12C2.25 17.3848 6.61522 21.75 12 21.75ZM12 7.75C11.0335 7.75 10.25 8.5335 10.25 9.5C10.25 9.91421 9.91421 10.25 9.5 10.25C9.08579 10.25 8.75 9.91421 8.75 9.5C8.75 7.70507 10.2051 6.25 12 6.25H12.1361C13.8559 6.25 15.25 7.64413 15.25 9.36387C15.25 10.3098 14.82 11.2045 14.0814 11.7954L13.6398 12.1486C13.0774 12.5985 12.75 13.2797 12.75 14C12.75 14.4142 12.4142 14.75 12 14.75C11.5858 14.75 11.25 14.4142 11.25 14C11.25 12.8241 11.7845 11.7119 12.7028 10.9773L13.1443 10.6241C13.5271 10.3178 13.75 9.85413 13.75 9.36387C13.75 8.47255 13.0274 7.75 12.1361 7.75H12ZM11.0498 16.9C11.0498 16.3753 11.4751 15.95 11.9998 15.95C12.5245 15.95 12.9498 16.3753 12.9498 16.9C12.9498 17.4247 12.5245 17.85 11.9998 17.85C11.4751 17.85 11.0498 17.4247 11.0498 16.9Z" fill="#23262A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,17 @@
|
||||
name: QuestionmarkCircleFill
|
||||
category: Status
|
||||
sub_category: Guidance
|
||||
keywords:
|
||||
- assistance
|
||||
- support
|
||||
- information
|
||||
- guidance
|
||||
- question mark
|
||||
- hjelp
|
||||
- støtte
|
||||
- informasjon
|
||||
- veiledning
|
||||
- spørsmålstegn
|
||||
variant: Fill
|
||||
updated_at: 29.01.2025
|
||||
created_at: 29.01.2025
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 4C4.41421 4 4.75 4.33579 4.75 4.75V11.25H6.75C7.16421 11.25 7.5 11.5858 7.5 12C7.5 12.4142 7.16421 12.75 6.75 12.75H4.75V19.25C4.75 19.6642 4.41421 20 4 20C3.58579 20 3.25 19.6642 3.25 19.25V4.75C3.25 4.33579 3.58579 4 4 4ZM9.25 11.25C8.83579 11.25 8.5 11.5858 8.5 12C8.5 12.4142 8.83579 12.75 9.25 12.75H10.75C11.1642 12.75 11.5 12.4142 11.5 12C11.5 11.5858 11.1642 11.25 10.75 11.25H9.25ZM12.5 12C12.5 11.5858 12.8358 11.25 13.25 11.25H14.75C15.1642 11.25 15.5 11.5858 15.5 12C15.5 12.4142 15.1642 12.75 14.75 12.75H13.25C12.8358 12.75 12.5 12.4142 12.5 12ZM17.25 11.25C16.8358 11.25 16.5 11.5858 16.5 12C16.5 12.4142 16.8358 12.75 17.25 12.75H19.25V19.25C19.25 19.6642 19.5858 20 20 20C20.4142 20 20.75 19.6642 20.75 19.25V4.75C20.75 4.33579 20.4142 4 20 4C19.5858 4 19.25 4.33579 19.25 4.75V11.25H17.25Z" fill="#23262A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 981 B |
@@ -0,0 +1,12 @@
|
||||
name: SpaceHorizontal
|
||||
category: Interface
|
||||
sub_category: Design
|
||||
keywords:
|
||||
- avstand
|
||||
- padding
|
||||
- margin
|
||||
- horisontal
|
||||
- luft
|
||||
variant: Stroke
|
||||
updated_at: 14.02.2025
|
||||
created_at: 14.02.2025
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.75 3.25C4.33579 3.25 4 3.58579 4 4C4 4.41421 4.33579 4.75 4.75 4.75H11.25V6.75C11.25 7.16421 11.5858 7.5 12 7.5C12.4142 7.5 12.75 7.16421 12.75 6.75V4.75H19.25C19.6642 4.75 20 4.41421 20 4C20 3.58579 19.6642 3.25 19.25 3.25H4.75ZM12 11.5C11.5858 11.5 11.25 11.1642 11.25 10.75V9.25C11.25 8.83579 11.5858 8.5 12 8.5C12.4142 8.5 12.75 8.83579 12.75 9.25V10.75C12.75 11.1642 12.4142 11.5 12 11.5ZM11.25 14.75C11.25 15.1642 11.5858 15.5 12 15.5C12.4142 15.5 12.75 15.1642 12.75 14.75V13.25C12.75 12.8358 12.4142 12.5 12 12.5C11.5858 12.5 11.25 12.8358 11.25 13.25V14.75ZM4.75 19.25H11.25V17.25C11.25 16.8358 11.5858 16.5 12 16.5C12.4142 16.5 12.75 16.8358 12.75 17.25V19.25H19.25C19.6642 19.25 20 19.5858 20 20C20 20.4142 19.6642 20.75 19.25 20.75H4.75C4.33579 20.75 4 20.4142 4 20C4 19.5858 4.33579 19.25 4.75 19.25Z" fill="#23262A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 987 B |
@@ -0,0 +1,12 @@
|
||||
name: SpaceVertical
|
||||
category: Interface
|
||||
sub_category: Design
|
||||
keywords:
|
||||
- avstand
|
||||
- padding
|
||||
- margin
|
||||
- vertikal
|
||||
- luft
|
||||
variant: Stroke
|
||||
updated_at: 14.02.2025
|
||||
created_at: 14.02.2025
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-icons",
|
||||
"version": "7.12.1",
|
||||
"version": "7.14.1",
|
||||
"author": "Aksel | Nav",
|
||||
"homepage": "https://aksel.nav.no/ikoner",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @navikt/aksel-stylelint
|
||||
|
||||
## 7.14.1
|
||||
|
||||
## 7.14.0
|
||||
|
||||
## 7.13.0
|
||||
|
||||
## 7.12.2
|
||||
|
||||
## 7.12.1
|
||||
|
||||
## 7.12.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-stylelint",
|
||||
"version": "7.12.1",
|
||||
"version": "7.14.1",
|
||||
"author": "Aksel | Nav",
|
||||
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
|
||||
"repository": {
|
||||
@@ -35,8 +35,8 @@
|
||||
"dev": "yarn watch:lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-css": "^7.12.1",
|
||||
"@navikt/ds-tokens": "^7.12.1",
|
||||
"@navikt/ds-css": "^7.14.1",
|
||||
"@navikt/ds-tokens": "^7.14.1",
|
||||
"concurrently": "9.0.1",
|
||||
"postcss-selector-parser": "^6.0.13",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
# @navikt/aksel
|
||||
|
||||
## 7.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-css@7.14.1
|
||||
|
||||
## 7.14.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`08e134e`](https://github.com/navikt/aksel/commit/08e134ef3c2238d7d405b44e27edef593339406c), [`821343b`](https://github.com/navikt/aksel/commit/821343b67d702379013ceba5961a5872dca4fd1f)]:
|
||||
- @navikt/ds-css@7.14.0
|
||||
|
||||
## 7.13.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1ba5d9b`](https://github.com/navikt/aksel/commit/1ba5d9b5a2fb74ee2c1fdb8d498520ad85323a12)]:
|
||||
- @navikt/ds-css@7.13.0
|
||||
|
||||
## 7.12.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-css@7.12.2
|
||||
|
||||
## 7.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel",
|
||||
"version": "7.12.1",
|
||||
"version": "7.14.1",
|
||||
"description": "Aksel command line interface. Handles css-imports, codemods and more",
|
||||
"author": "Aksel | Nav designsystem team",
|
||||
"license": "MIT",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"homepage": "https://aksel.nav.no/grunnleggende/kode/kommandolinje",
|
||||
"dependencies": {
|
||||
"@navikt/ds-css": "^7.12.1",
|
||||
"@navikt/ds-css": "^7.14.1",
|
||||
"axios": "1.7.4",
|
||||
"chalk": "4.1.0",
|
||||
"clipboardy": "^2.3.0",
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# @navikt/ds-css
|
||||
|
||||
## 7.14.1
|
||||
|
||||
## 7.14.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- DatePicker: Updated `date.css` to support `react-day-picker v9`. ([#3525](https://github.com/navikt/aksel/pull/3525))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Darkside: Added 'arrow' back to Tooltip. ([#3589](https://github.com/navikt/aksel/pull/3589))
|
||||
|
||||
## 7.13.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Darkside: Added support for `ConfirmationpPanel`, `Linkpanel`, `Panel` and `Dropdown`. Note that all of these will be deprecated in the future. ([#3581](https://github.com/navikt/aksel/pull/3581))
|
||||
|
||||
## 7.12.2
|
||||
|
||||
## 7.12.1
|
||||
|
||||
## 7.12.0
|
||||
|
||||
@@ -6,7 +6,7 @@ import { describe, expect, test } from "vitest";
|
||||
* Darkside config might have some new files added. We can skip these when checking for equality.
|
||||
*/
|
||||
const newFiles = ["theme"];
|
||||
const deprecatedFiles = ["linkpanel"];
|
||||
const deprecatedFiles: string[] = [];
|
||||
|
||||
describe("Check that old and new (darkside) bundle matches", () => {
|
||||
test("Darkside includes the same files as old bundle", () => {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
.navds-dropdown__menu {
|
||||
overflow: hidden;
|
||||
padding: var(--a-spacing-2) 0;
|
||||
color: var(--ac-dropdown-text, var(--a-text-default));
|
||||
padding: var(--ax-space-8);
|
||||
color: var(--ax-text-neutral);
|
||||
width: 27ch;
|
||||
max-height: 616px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.navds-dropdown__divider {
|
||||
margin: var(--a-spacing-3) 0;
|
||||
margin: var(--ax-space-12) 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--a-border-divider);
|
||||
border-bottom: 1px solid var(--ax-border-neutral-subtle);
|
||||
}
|
||||
|
||||
.navds-dropdown__menu > :not(.navds-dropdown__divider, .navds-dropdown__list) {
|
||||
margin: 0 var(--a-spacing-4) var(--a-spacing-3);
|
||||
margin: 0 var(--ax-space-16) var(--ax-space-12);
|
||||
}
|
||||
|
||||
.navds-dropdown__list {
|
||||
@@ -28,7 +28,8 @@
|
||||
}
|
||||
|
||||
.navds-dropdown__list-heading {
|
||||
margin: var(--a-spacing-1) var(--a-spacing-4) var(--a-spacing-3);
|
||||
margin-block: var(--ax-space-4) var(--ax-space-12);
|
||||
margin-inline: var(--ax-space-4);
|
||||
}
|
||||
|
||||
.navds-dropdown__item {
|
||||
@@ -36,7 +37,6 @@
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
background: transparent;
|
||||
color: var(--ac-dropdown-item-text, var(--a-text-action));
|
||||
font: inherit;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
@@ -44,39 +44,29 @@
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--a-spacing-2);
|
||||
padding: var(--a-spacing-1) var(--a-spacing-4);
|
||||
}
|
||||
gap: var(--ax-space-8);
|
||||
padding: var(--ax-space-4);
|
||||
border-radius: var(--ax-border-radius-medium);
|
||||
color: var(--ax-text-neutral);
|
||||
|
||||
.navds-dropdown__item:hover {
|
||||
background-color: var(--ac-dropdown-item-hover-bg, var(--a-surface-action-subtle-hover));
|
||||
color: var(--ac-dropdown-item-hover-text, var(--a-text-action-hover));
|
||||
}
|
||||
|
||||
.navds-dropdown__item:active {
|
||||
background-color: var(--ac-dropdown-item-active-bg, var(--a-surface-action-active));
|
||||
color: var(--ac-dropdown-item-active-text, var(--a-text-on-action));
|
||||
}
|
||||
|
||||
.navds-dropdown__item:focus-visible {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
box-shadow: inset 0 0 0 2px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-dropdown__item:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
box-shadow: inset 0 0 0 2px var(--a-border-focus);
|
||||
&:hover {
|
||||
background-color: var(--ax-bg-accent-moderate-hoverA);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-dropdown__item:disabled {
|
||||
color: var(--ac-dropdown-item-text, var(--a-text-action));
|
||||
opacity: var(--ax-opacity-disabled);
|
||||
background: transparent;
|
||||
cursor: not-allowed;
|
||||
&:active {
|
||||
background-color: var(--ax-bg-accent-moderate-pressedA);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 3px solid var(--ax-border-focus);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: var(--ax-opacity-disabled);
|
||||
background: transparent;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.navds-confirmation-panel__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--a-spacing-3);
|
||||
padding: var(--a-spacing-4);
|
||||
border-radius: var(--a-border-radius-medium);
|
||||
border: 1px solid var(--ac-confirmation-panel-border, var(--a-border-warning));
|
||||
background-color: var(--ac-confirmation-panel-bg, var(--a-surface-warning-subtle));
|
||||
gap: var(--ax-space-12);
|
||||
padding: var(--ax-space-16);
|
||||
border-radius: var(--ax-border-radius-large);
|
||||
border: 1px solid var(--ax-border-default);
|
||||
background-color: var(--ax-bg-moderate);
|
||||
transition: background-color linear 100ms;
|
||||
justify-self: stretch;
|
||||
position: relative;
|
||||
@@ -15,20 +15,6 @@
|
||||
max-width: 80ch;
|
||||
}
|
||||
|
||||
.navds-confirmation-panel--checked .navds-confirmation-panel__inner {
|
||||
border-color: var(--ac-confirmation-panel-checked-border, var(--a-border-success));
|
||||
background-color: var(--ac-confirmation-panel-checked-bg, var(--a-surface-success-subtle));
|
||||
}
|
||||
|
||||
.navds-confirmation-panel--error .navds-confirmation-panel__inner {
|
||||
border-color: var(--ac-confirmation-panel-error-border, var(--a-border-danger));
|
||||
background-color: var(--ac-confirmation-panel-error-bg, var(--a-surface-danger-subtle));
|
||||
}
|
||||
|
||||
.navds-confirmation-panel--error .navds-checkbox__input:hover:not(:disabled) + .navds-checkbox__label {
|
||||
color: var(--a-text-default);
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.navds-confirmation-panel__inner::before {
|
||||
content: "";
|
||||
@@ -39,8 +25,8 @@
|
||||
border-left: 8px solid;
|
||||
border-color: orange;
|
||||
forced-color-adjust: none;
|
||||
border-start-start-radius: calc(var(--a-border-radius-medium) - 1px);
|
||||
border-end-start-radius: calc(var(--a-border-radius-medium) - 1px);
|
||||
border-start-start-radius: calc(var(--ax-border-radius-large) - 1px);
|
||||
border-end-start-radius: calc(var(--ax-border-radius-large) - 1px);
|
||||
}
|
||||
|
||||
.navds-confirmation-panel--checked .navds-confirmation-panel__inner::before {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
@import "./baseline/print.darkside.css" layer(aksel.print);
|
||||
|
||||
/* ------------------------------- Components ------------------------------- */
|
||||
@import "./theme.darkside.css" layer(aksel.components);
|
||||
@import "./typography.darkside.css" layer(aksel.components);
|
||||
@import "./button.darkside.css" layer(aksel.components);
|
||||
@import "./chips.darkside.css" layer(aksel.components);
|
||||
@@ -36,6 +37,7 @@
|
||||
@import "./help-text.darkside.css" layer(aksel.components);
|
||||
@import "./internalheader.darkside.css" layer(aksel.components);
|
||||
@import "./link.darkside.css" layer(aksel.components);
|
||||
@import "./link-panel.darkside.css" layer(aksel.components);
|
||||
@import "./loader.darkside.css" layer(aksel.components);
|
||||
@import "./modal.darkside.css" layer(aksel.components);
|
||||
@import "./pagination.darkside.css" layer(aksel.components);
|
||||
@@ -52,7 +54,6 @@
|
||||
@import "./table.darkside.css" layer(aksel.components);
|
||||
@import "./tabs.darkside.css" layer(aksel.components);
|
||||
@import "./list.darkside.css" layer(aksel.components);
|
||||
@import "./theme.darkside.css" layer(aksel.components);
|
||||
|
||||
/* --------------------------------- Layout --------------------------------- */
|
||||
@import "./primitives/index.css" layer(aksel.layout);
|
||||
@@ -0,0 +1,37 @@
|
||||
.navds-link-panel {
|
||||
text-decoration: none;
|
||||
color: var(--ax-text-neutral);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ax-space-16);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--ax-border-accent);
|
||||
|
||||
& .navds-link-panel__title {
|
||||
text-decoration: underline;
|
||||
color: var(--ax-text-accent-subtle);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 3px solid var(--ax-border-focus);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.navds-link-panel__chevron {
|
||||
flex-shrink: 0;
|
||||
font-size: 1.5rem;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
|
||||
.navds-link-panel:hover > .navds-link-panel__chevron,
|
||||
.navds-link-panel:focus-within > .navds-link-panel__chevron {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.navds-link-panel__description {
|
||||
margin-top: var(--a-space-4);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
.navds-panel {
|
||||
background-color: var(--ac-panel-bg, var(--a-surface-default));
|
||||
padding: var(--a-spacing-4);
|
||||
border-radius: var(--a-border-radius-small);
|
||||
background-color: var(--ax-bg-default);
|
||||
padding: var(--ax-space-16);
|
||||
border-radius: var(--ax-border-radius-small);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.navds-panel--border {
|
||||
border-color: var(--ac-panel-border, var(--a-border-default));
|
||||
border-color: var(--ax-border-neutral);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
.navds-tooltip {
|
||||
z-index: 3000;
|
||||
background-color: var(--ax-bg-raised);
|
||||
color: var(--ax-text-neutral);
|
||||
background-color: var(--ax-bg-neutral-strong);
|
||||
color: var(--ax-text-neutral-contrast);
|
||||
border-radius: var(--ax-border-radius-medium);
|
||||
padding: 0 var(--ax-space-6);
|
||||
align-items: center;
|
||||
@@ -10,8 +10,28 @@
|
||||
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
animation-duration: 150ms;
|
||||
text-align: center;
|
||||
border: 1px solid var(--ax-border-neutral-subtleA);
|
||||
box-shadow: var(--ax-shadow-dialog);
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.navds-tooltip__arrow {
|
||||
height: 0.5rem;
|
||||
width: 0.5rem;
|
||||
z-index: -1;
|
||||
background-color: var(--ax-bg-neutral-strong);
|
||||
position: absolute;
|
||||
border-radius: 1px;
|
||||
transform: rotate(45deg);
|
||||
|
||||
@media (forced-colors: active) {
|
||||
& {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navds-tooltip:where([data-state="open"]):where([data-side="bottom"]) {
|
||||
@@ -96,7 +116,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--ax-border-radius-small);
|
||||
background: var(--ax-bg-neutral-moderateA);
|
||||
background: var(--ax-bg-neutral-moderate);
|
||||
|
||||
@media (forced-colors: active) {
|
||||
& {
|
||||
|
||||
+11
-21
@@ -22,30 +22,13 @@
|
||||
font-size: var(--a-font-size-small);
|
||||
}
|
||||
|
||||
.navds-date .rdp-weeknumber {
|
||||
font-size: var(--a-font-size-small);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: var(--a-border-radius-medium);
|
||||
margin: var(--a-spacing-2);
|
||||
.navds-date__weeknumber-number {
|
||||
font-size: 0.875rem;
|
||||
color: var(--a-text-subtle);
|
||||
}
|
||||
|
||||
.navds-date .rdp-weeknumber.rdp-button {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
box-shadow: 0 0 0 1px var(--a-border-default);
|
||||
color: var(--a-text-subtle);
|
||||
font-size: var(--a-font-size-small);
|
||||
}
|
||||
|
||||
.navds-date .rdp-weeknumber.rdp-button:active {
|
||||
background-color: var(--a-surface-action-active);
|
||||
color: var(--a-text-on-action);
|
||||
box-shadow: none;
|
||||
.navds-date__weeknumber:active .navds-date__weeknumber-number {
|
||||
color: var(--a-text-on-neutral);
|
||||
}
|
||||
|
||||
.navds-date__caption__month .navds-select__container select {
|
||||
@@ -303,6 +286,13 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
span.rdp-weeknumber {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.navds-date__modal.navds-date {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "7.12.1",
|
||||
"version": "7.14.1",
|
||||
"description": "CSS for Nav Designsystem",
|
||||
"author": "Aksel | Nav designsystem team",
|
||||
"keywords": [
|
||||
@@ -28,7 +28,7 @@
|
||||
"test": "yarn build && vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^7.12.1",
|
||||
"@navikt/ds-tokens": "^7.14.1",
|
||||
"@types/clean-css": "4.2.11",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"browserslist": "^4.24.2",
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
# @navikt/ds-react
|
||||
|
||||
## 7.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Datepicker: Fixed regression where some fromDate/toDate instances lead to that date ending up as disabled. ([#3602](https://github.com/navikt/aksel/pull/3602))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@7.14.1
|
||||
- @navikt/aksel-icons@7.14.1
|
||||
|
||||
## 7.14.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- DatePicker: Refactored locally to support `react-day-picker v9`. No external API has been changed. ([#3525](https://github.com/navikt/aksel/pull/3525))
|
||||
|
||||
- MonthPicker: Refactored locally and no longer depends on `react-day-picker v9`. No external API has been changed. ([#3525](https://github.com/navikt/aksel/pull/3525))
|
||||
|
||||
- Dependencies: `react-day-picker` bumped to `v9.5.0`. ([#3525](https://github.com/navikt/aksel/pull/3525))
|
||||
|
||||
- Dependencies: `date-fns` bumped to `^4.0.0`. ([#3525](https://github.com/navikt/aksel/pull/3525))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Darkside: Added 'arrow' back to Tooltip. ([#3589](https://github.com/navikt/aksel/pull/3589))
|
||||
|
||||
- Updated dependencies [[`5937d08`](https://github.com/navikt/aksel/commit/5937d085c0bee19fe3625574d58aadffee0c6cf9)]:
|
||||
- @navikt/aksel-icons@7.14.0
|
||||
- @navikt/ds-tokens@7.14.0
|
||||
|
||||
## 7.13.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Darkside: Added support for `ConfirmationpPanel`, `Linkpanel`, `Panel` and `Dropdown`. Note that all of these will be deprecated in the future. ([#3581](https://github.com/navikt/aksel/pull/3581))
|
||||
|
||||
- Updated dependencies [[`636eb05`](https://github.com/navikt/aksel/commit/636eb05bf56ba2c8513704c61882fde369c2e67b)]:
|
||||
- @navikt/aksel-icons@7.13.0
|
||||
- @navikt/ds-tokens@7.13.0
|
||||
|
||||
## 7.12.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Box: Added export for `BoxNew`. ([`8576400`](https://github.com/navikt/aksel/commit/857640026368153b64171f1cf954b9abac493ee8))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@7.12.2
|
||||
- @navikt/aksel-icons@7.12.2
|
||||
|
||||
## 7.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "7.12.1",
|
||||
"version": "7.14.1",
|
||||
"description": "React components from the Norwegian Labour and Welfare Administration.",
|
||||
"author": "Aksel, a team part of the Norwegian Labour and Welfare Administration.",
|
||||
"license": "MIT",
|
||||
@@ -620,16 +620,16 @@
|
||||
"build": "yarn i18n-jsdoc && concurrently \"tsc -p tsconfig.build.json\" \"tsc -p tsconfig.esm.json && tsc-alias -p tsconfig.esm.json && yarn write-packagejson\" && yarn i18n-jsdoc --cleanup",
|
||||
"watch": "tsc --watch -p tsconfig.esm.json",
|
||||
"test": "TZ=UTC vitest run -c tests/vitest.config.ts",
|
||||
"test:watch": "vitest watch"
|
||||
"test:watch": "TZ=UTC vitest watch -c tests/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "0.25.4",
|
||||
"@floating-ui/react-dom": "^2.0.9",
|
||||
"@navikt/aksel-icons": "^7.12.1",
|
||||
"@navikt/ds-tokens": "^7.12.1",
|
||||
"@navikt/aksel-icons": "^7.14.1",
|
||||
"@navikt/ds-tokens": "^7.14.1",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.0.0",
|
||||
"react-day-picker": "8.10.1"
|
||||
"date-fns": "^4.0.0",
|
||||
"react-day-picker": "9.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "10.4.0",
|
||||
|
||||
+12
-9
@@ -1,13 +1,13 @@
|
||||
import cl from "clsx";
|
||||
import React, { useRef } from "react";
|
||||
import { Button } from "../../button";
|
||||
import { Modal } from "../../modal";
|
||||
import { useModalContext } from "../../modal/Modal.context";
|
||||
import { Popover } from "../../popover";
|
||||
import { useMedia } from "../../util/hooks";
|
||||
import { useI18n } from "../../util/i18n/i18n.hooks";
|
||||
import { TFunction } from "../../util/i18n/i18n.types";
|
||||
import { getGlobalTranslations } from "../utils";
|
||||
import { Button } from "../button";
|
||||
import { Modal } from "../modal";
|
||||
import { useModalContext } from "../modal/Modal.context";
|
||||
import { Popover } from "../popover";
|
||||
import { useMedia } from "../util/hooks";
|
||||
import { useI18n } from "../util/i18n/i18n.hooks";
|
||||
import { TFunction } from "../util/i18n/i18n.types";
|
||||
import { getGlobalTranslations } from "./Date.locale";
|
||||
|
||||
const variantToLabel = {
|
||||
single: "chooseDate",
|
||||
@@ -31,7 +31,7 @@ type DateWrapperProps = {
|
||||
};
|
||||
};
|
||||
|
||||
export const DateWrapper = ({
|
||||
const DateDialog = ({
|
||||
open,
|
||||
children,
|
||||
onClose,
|
||||
@@ -66,6 +66,7 @@ export const DateWrapper = ({
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
ref={modalRef}
|
||||
@@ -80,6 +81,7 @@ export const DateWrapper = ({
|
||||
"navds-date": variant === "month",
|
||||
})}
|
||||
closeOnBackdropClick
|
||||
placement="top"
|
||||
>
|
||||
<div className="navds-date__modal-body">
|
||||
{children}
|
||||
@@ -95,3 +97,4 @@ export const DateWrapper = ({
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export { DateDialog };
|
||||
+30
-5
@@ -1,11 +1,36 @@
|
||||
import cl from "clsx";
|
||||
import React, { InputHTMLAttributes, forwardRef, useRef } from "react";
|
||||
import { CalendarIcon } from "@navikt/aksel-icons";
|
||||
import { ReadOnlyIcon } from "../../form/ReadOnlyIcon";
|
||||
import { FormFieldProps, useFormField } from "../../form/useFormField";
|
||||
import { BodyShort, ErrorMessage, Label } from "../../typography";
|
||||
import { omit } from "../../util";
|
||||
import { useDateInputContext, useDateTranslationContext } from "../context";
|
||||
import { ReadOnlyIcon } from "../form/ReadOnlyIcon";
|
||||
import { FormFieldProps, useFormField } from "../form/useFormField";
|
||||
import { BodyShort, ErrorMessage, Label } from "../typography";
|
||||
import { omit } from "../util";
|
||||
import { createContext } from "../util/create-context";
|
||||
import { useDateTranslationContext } from "./Date.locale";
|
||||
|
||||
interface DateInputContextProps {
|
||||
/**
|
||||
* Open state for popover
|
||||
*/
|
||||
open: boolean;
|
||||
/**
|
||||
* Callback for onOpen toggle
|
||||
*/
|
||||
onOpen: () => void;
|
||||
/**
|
||||
* Aria-connected ID
|
||||
*/
|
||||
ariaId?: string;
|
||||
/**
|
||||
* Flag for enabled-check
|
||||
*/
|
||||
defined: boolean;
|
||||
}
|
||||
|
||||
export const [DateInputContextProvider, useDateInputContext] =
|
||||
createContext<DateInputContextProps>({
|
||||
errorMessage: "useDateInputContext must be used with DateInputContext",
|
||||
});
|
||||
|
||||
export interface DateInputProps
|
||||
extends FormFieldProps,
|
||||
+11
-2
@@ -1,6 +1,8 @@
|
||||
import { enGB, nb, nn } from "date-fns/locale";
|
||||
import en_translations from "../../util/i18n/locales/en";
|
||||
import nn_translations from "../../util/i18n/locales/nn";
|
||||
import { createContext } from "../util/create-context";
|
||||
import { TFunction } from "../util/i18n/i18n.types";
|
||||
import en_translations from "../util/i18n/locales/en";
|
||||
import nn_translations from "../util/i18n/locales/nn";
|
||||
|
||||
/** @private */
|
||||
export const getLocaleFromString = (locale: "nb" | "nn" | "en" = "nb") => {
|
||||
@@ -45,3 +47,10 @@ export const getGlobalTranslations = (locale: string | undefined) => {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
interface DateTranslationContextProps {
|
||||
translate: TFunction<"DatePicker">;
|
||||
}
|
||||
|
||||
export const [DateTranslationContextProvider, useDateTranslationContext] =
|
||||
createContext<DateTranslationContextProps>();
|
||||
@@ -0,0 +1,32 @@
|
||||
export type DateRange = {
|
||||
from: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
export type DateBefore = {
|
||||
before: Date;
|
||||
};
|
||||
|
||||
export type DateAfter = {
|
||||
after: Date;
|
||||
};
|
||||
|
||||
export function isDateAfterType(value: unknown): value is DateAfter {
|
||||
return Boolean(value && typeof value === "object" && "after" in value);
|
||||
}
|
||||
|
||||
export function isDateBeforeType(value: unknown): value is DateBefore {
|
||||
return Boolean(value && typeof value === "object" && "before" in value);
|
||||
}
|
||||
|
||||
export function isDateRange(value: unknown): value is DateRange {
|
||||
return Boolean(value && typeof value === "object" && "from" in value);
|
||||
}
|
||||
|
||||
export type Matcher =
|
||||
| ((date: Date) => boolean)
|
||||
| Date
|
||||
| Date[]
|
||||
| DateRange
|
||||
| DateBefore
|
||||
| DateAfter;
|
||||
@@ -1,9 +0,0 @@
|
||||
export { useDateInputContext, DateInputContext } from "./useDateInputContext";
|
||||
export {
|
||||
useSharedMonthContext,
|
||||
SharedMonthProvider,
|
||||
} from "./useSharedMonthContext";
|
||||
export {
|
||||
useDateTranslationContext,
|
||||
DateTranslationContextProvider,
|
||||
} from "./useDateTranslationContext";
|
||||
@@ -1,34 +0,0 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
interface DateInputContextProps {
|
||||
/**
|
||||
* Open state for popover
|
||||
*/
|
||||
open: boolean;
|
||||
/**
|
||||
* Callback for opOpen toggle
|
||||
*/
|
||||
onOpen: () => void;
|
||||
/**
|
||||
* Aria-connected ID
|
||||
*/
|
||||
ariaId?: string;
|
||||
/**
|
||||
* Flag for enabled-check
|
||||
*/
|
||||
defined: boolean;
|
||||
}
|
||||
|
||||
export const DateInputContext = createContext<DateInputContextProps | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export const useDateInputContext = () => {
|
||||
const context = useContext(DateInputContext);
|
||||
|
||||
if (!context) {
|
||||
console.warn("useDateInputContext must be used with DateInputContext");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createContext } from "../../util/create-context";
|
||||
import { TFunction } from "../../util/i18n/i18n.types";
|
||||
|
||||
interface DateTranslationContextProps {
|
||||
translate: TFunction<"DatePicker">;
|
||||
}
|
||||
|
||||
export const [DateTranslationContextProvider, useDateTranslationContext] =
|
||||
createContext<DateTranslationContextProps>();
|
||||
@@ -1,68 +0,0 @@
|
||||
import { setYear, startOfMonth } from "date-fns";
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
import { useDayPicker } from "react-day-picker";
|
||||
import { Matcher, getInitialYear } from "../utils";
|
||||
|
||||
export type SharedMonthContextType = {
|
||||
hasDropdown: boolean;
|
||||
year: Date;
|
||||
toYear: (date: Date) => void;
|
||||
disabled: Matcher[];
|
||||
selected?: Date;
|
||||
onSelect: (v?: Date) => void;
|
||||
};
|
||||
|
||||
export const SharedMonthContext = createContext<SharedMonthContextType>({
|
||||
hasDropdown: false,
|
||||
year: new Date(),
|
||||
toYear: () => null,
|
||||
disabled: [],
|
||||
onSelect: () => null,
|
||||
});
|
||||
|
||||
export const useSharedMonthContext = () => useContext(SharedMonthContext);
|
||||
|
||||
export const SharedMonthProvider = ({
|
||||
children,
|
||||
dropdownCaption,
|
||||
disabled,
|
||||
selected,
|
||||
onSelect,
|
||||
year: _year,
|
||||
onYearChange,
|
||||
}) => {
|
||||
const context = useDayPicker();
|
||||
|
||||
const [year, toYear] = useState<Date>(getInitialYear(context));
|
||||
|
||||
const hasDropdown = !!(dropdownCaption && context.fromDate && context.toDate);
|
||||
|
||||
if (
|
||||
context.fromDate &&
|
||||
context.toDate &&
|
||||
context?.fromDate >= context?.toDate
|
||||
) {
|
||||
console.warn("fromDate needs to be before toDate - MonthPicker");
|
||||
}
|
||||
|
||||
return (
|
||||
<SharedMonthContext.Provider
|
||||
value={{
|
||||
year: _year ?? year,
|
||||
toYear: (y) => {
|
||||
toYear(y);
|
||||
onYearChange?.(y);
|
||||
},
|
||||
hasDropdown,
|
||||
disabled,
|
||||
selected,
|
||||
onSelect: (v?: Date) =>
|
||||
v
|
||||
? onSelect(setYear(startOfMonth(v), (_year ?? year).getFullYear()))
|
||||
: onSelect(undefined),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SharedMonthContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
addYears,
|
||||
endOfMonth,
|
||||
endOfYear,
|
||||
startOfDay,
|
||||
startOfMonth,
|
||||
startOfYear,
|
||||
} from "date-fns";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { calendarRange } from "./calendar-range";
|
||||
|
||||
describe("calendarRange", () => {
|
||||
test("should return start and end of the given months", () => {
|
||||
const startMonth = new Date(2023, 0, 15);
|
||||
const endMonth = new Date(2023, 11, 15);
|
||||
const result = calendarRange({ startMonth, endMonth });
|
||||
|
||||
expect(result[0]).toEqual(startOfDay(startOfMonth(startMonth)));
|
||||
expect(result[1]).toEqual(startOfDay(endOfMonth(endMonth)));
|
||||
});
|
||||
|
||||
test("should return start of the year 100 years ago and end of the current year if captionLayout is dropdown", () => {
|
||||
const today = new Date(2023, 6, 15);
|
||||
const result = calendarRange({ captionLayout: "dropdown", today });
|
||||
|
||||
expect(result[0]).toEqual(startOfDay(startOfYear(addYears(today, -100))));
|
||||
expect(result[1]).toEqual(startOfDay(endOfYear(today)));
|
||||
});
|
||||
|
||||
test("should return undefined if no startMonth and endMonth are provided", () => {
|
||||
const today = new Date(2023, 6, 15);
|
||||
const result = calendarRange({ today });
|
||||
|
||||
expect(result[0]).toBeUndefined();
|
||||
expect(result[1]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should handle undefined today date", () => {
|
||||
const result = calendarRange({ captionLayout: "dropdown" });
|
||||
|
||||
const today = new Date();
|
||||
expect(result[0]).toEqual(startOfDay(startOfYear(addYears(today, -100))));
|
||||
expect(result[1]).toEqual(startOfDay(endOfYear(today)));
|
||||
});
|
||||
|
||||
test("should handle undefined captionLayout", () => {
|
||||
const startMonth = new Date(2023, 0, 15);
|
||||
const endMonth = new Date(2023, 11, 15);
|
||||
const result = calendarRange({ startMonth, endMonth });
|
||||
|
||||
expect(result[0]).toEqual(startOfDay(startOfMonth(startMonth)));
|
||||
expect(result[1]).toEqual(startOfDay(endOfMonth(endMonth)));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
addYears,
|
||||
endOfMonth,
|
||||
endOfYear,
|
||||
startOfDay,
|
||||
startOfMonth,
|
||||
startOfYear,
|
||||
} from "date-fns";
|
||||
|
||||
/**
|
||||
* Generates the min and max-dates possible to show and navigate to in the calendar.
|
||||
* In the cases there is not a startMonth or endMonth, and layout is "label" no min or max dates are set.
|
||||
* @returns Return the start and end months for the calendar navigation.
|
||||
*/
|
||||
export function calendarRange({
|
||||
captionLayout,
|
||||
startMonth,
|
||||
endMonth,
|
||||
today,
|
||||
}: {
|
||||
captionLayout?: "label" | "dropdown";
|
||||
startMonth?: Date;
|
||||
endMonth?: Date;
|
||||
today?: Date;
|
||||
}) {
|
||||
const hasYearDropdown = captionLayout === "dropdown";
|
||||
|
||||
const todayDate = today ?? new Date();
|
||||
|
||||
if (startMonth) {
|
||||
startMonth = startOfMonth(startMonth);
|
||||
} else if (!startMonth && hasYearDropdown) {
|
||||
startMonth = startOfYear(addYears(todayDate, -100));
|
||||
}
|
||||
|
||||
if (endMonth) {
|
||||
endMonth = endOfMonth(endMonth);
|
||||
} else if (!endMonth && hasYearDropdown) {
|
||||
endMonth = endOfYear(todayDate);
|
||||
}
|
||||
|
||||
return [
|
||||
startMonth ? startOfDay(startMonth) : startMonth,
|
||||
endMonth ? startOfDay(endMonth) : endMonth,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { setYear } from "date-fns";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { dateIsInCurrentMonth, isValidDate } from "./check-dates";
|
||||
|
||||
describe("dateIsInCurrentMonth", () => {
|
||||
test("should return true if the date is in the same month and year as the date to compare", () => {
|
||||
const date = new Date();
|
||||
const dateToCompare = new Date();
|
||||
expect(dateIsInCurrentMonth(date, dateToCompare)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if the date is not in the same month as the date to compare", () => {
|
||||
const date = new Date();
|
||||
const dateToCompare = new Date(2023, 9, 1); // October 1, 2023
|
||||
expect(dateIsInCurrentMonth(date, dateToCompare)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if the date is in the same month but different year as the date to compare", () => {
|
||||
const date = new Date();
|
||||
const dateToCompare = new Date();
|
||||
expect(
|
||||
dateIsInCurrentMonth(
|
||||
date,
|
||||
setYear(dateToCompare, dateToCompare.getFullYear() + 1),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidDate", () => {
|
||||
test("should return true for a valid date", () => {
|
||||
const date = new Date(2023, 9, 15); // October 15, 2023
|
||||
expect(isValidDate(date)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false for an invalid date", () => {
|
||||
const date = new Date("invalid date");
|
||||
expect(isValidDate(date)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false for a date with year less than 1000", () => {
|
||||
const date = new Date(999, 9, 15); // October 15, 999
|
||||
expect(isValidDate(date)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false for undefined", () => {
|
||||
expect(isValidDate(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
differenceInCalendarDays,
|
||||
isThisMonth,
|
||||
setYear,
|
||||
startOfDay,
|
||||
} from "date-fns";
|
||||
|
||||
export const dateIsInCurrentMonth = (
|
||||
date: Date,
|
||||
dateToCompare: Date,
|
||||
): boolean => {
|
||||
return isThisMonth(setYear(date, Number(dateToCompare.getFullYear())));
|
||||
};
|
||||
|
||||
/** @private */
|
||||
export function isValidDate(day?: Date): boolean {
|
||||
return !!(day && !Number.isNaN(day.getTime()) && day.getFullYear() > 999);
|
||||
}
|
||||
|
||||
export function isDateOutsideRange({
|
||||
day,
|
||||
fromDate,
|
||||
toDate,
|
||||
}: {
|
||||
day: Date;
|
||||
fromDate?: Date;
|
||||
toDate?: Date;
|
||||
}): boolean {
|
||||
const isDateAfter =
|
||||
toDate && differenceInCalendarDays(day, startOfDay(toDate)) > 0;
|
||||
const isDateBefore =
|
||||
fromDate && differenceInCalendarDays(startOfDay(fromDate), day) > 0;
|
||||
|
||||
return isDateAfter || isDateBefore || false;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { startOfMonth, startOfYear } from "date-fns";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { isDateOutsideRange } from "./check-dates";
|
||||
import { clampDisplayMonth, clampDisplayYear } from "./clamp-dates";
|
||||
|
||||
describe("clampDisplayMonth", () => {
|
||||
test("should return undefined if month is not provided", () => {
|
||||
expect(
|
||||
clampDisplayMonth({ start: new Date(), end: new Date() }),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should return the start of the month if month is before start", () => {
|
||||
const start = new Date(2023, 5, 1);
|
||||
const month = new Date(2023, 4, 1);
|
||||
expect(clampDisplayMonth({ month, start, end: new Date() })).toEqual(
|
||||
startOfMonth(start),
|
||||
);
|
||||
});
|
||||
|
||||
test("should return the start of end month if month is after end", () => {
|
||||
const end = new Date(2023, 5, 1);
|
||||
const month = new Date(2023, 6, 1);
|
||||
expect(clampDisplayMonth({ month, start: new Date(), end })).toEqual(
|
||||
startOfMonth(end),
|
||||
);
|
||||
});
|
||||
|
||||
test("should return the start of the month if month is within range", () => {
|
||||
const month = new Date(2023, 5, 1);
|
||||
expect(
|
||||
clampDisplayMonth({
|
||||
month,
|
||||
start: new Date(2023, 4, 1),
|
||||
end: new Date(2023, 6, 1),
|
||||
}),
|
||||
).toEqual(startOfMonth(month));
|
||||
});
|
||||
});
|
||||
|
||||
describe("clampDisplayYear", () => {
|
||||
test("should return undefined if month is not provided", () => {
|
||||
expect(
|
||||
clampDisplayYear({ start: new Date(), end: new Date() }),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should return the start of the year if month is before start year", () => {
|
||||
const start = new Date(2023, 0, 1);
|
||||
const month = new Date(2022, 11, 1);
|
||||
expect(clampDisplayYear({ month, start, end: new Date() })).toEqual(
|
||||
startOfYear(start),
|
||||
);
|
||||
});
|
||||
|
||||
test("should return the start of the year if month is after end year", () => {
|
||||
const end = new Date(2023, 0, 1);
|
||||
const month = new Date(2024, 0, 1);
|
||||
expect(clampDisplayYear({ month, start: new Date(), end })).toEqual(
|
||||
startOfYear(end),
|
||||
);
|
||||
});
|
||||
|
||||
test("should return the start of the year if month is within range", () => {
|
||||
const month = new Date(2023, 5, 1);
|
||||
expect(
|
||||
clampDisplayYear({
|
||||
month,
|
||||
start: new Date(2022, 0, 1),
|
||||
end: new Date(2024, 0, 1),
|
||||
}),
|
||||
).toEqual(startOfYear(month));
|
||||
});
|
||||
});
|
||||
|
||||
describe("isDateOutsideRange", () => {
|
||||
test("returns false when day is within range", () => {
|
||||
const day = new Date(2023, 5, 15);
|
||||
const fromDate = new Date(2023, 0, 1);
|
||||
const toDate = new Date(2023, 11, 31);
|
||||
|
||||
expect(isDateOutsideRange({ day, fromDate, toDate })).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true when day is after toDate", () => {
|
||||
const day = new Date(2024, 0, 1);
|
||||
const fromDate = new Date(2023, 0, 1);
|
||||
const toDate = new Date(2023, 11, 31);
|
||||
|
||||
expect(isDateOutsideRange({ day, fromDate, toDate })).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true when day is before fromDate", () => {
|
||||
const day = new Date(2022, 11, 31);
|
||||
const fromDate = new Date(2023, 0, 1);
|
||||
const toDate = new Date(2023, 11, 31);
|
||||
|
||||
expect(isDateOutsideRange({ day, fromDate, toDate })).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when fromDate and toDate are not provided", () => {
|
||||
const day = new Date(2023, 5, 15);
|
||||
|
||||
expect(isDateOutsideRange({ day })).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true when only toDate is provided and day is after toDate", () => {
|
||||
const day = new Date(2024, 0, 1);
|
||||
const toDate = new Date(2023, 11, 31);
|
||||
|
||||
expect(isDateOutsideRange({ day, toDate })).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when only toDate is provided and day is before toDate", () => {
|
||||
const day = new Date(2023, 5, 15);
|
||||
const toDate = new Date(2023, 11, 31);
|
||||
|
||||
expect(isDateOutsideRange({ day, toDate })).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true when only fromDate is provided and day is before fromDate", () => {
|
||||
const day = new Date(2022, 11, 31);
|
||||
const fromDate = new Date(2023, 0, 1);
|
||||
|
||||
expect(isDateOutsideRange({ day, fromDate })).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when only fromDate is provided and day is after fromDate", () => {
|
||||
const day = new Date(2023, 5, 15);
|
||||
const fromDate = new Date(2023, 0, 1);
|
||||
|
||||
expect(isDateOutsideRange({ day, fromDate })).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when day is identical to fromDate", () => {
|
||||
const day = new Date(2023, 0, 1);
|
||||
const fromDate = new Date(2023, 0, 1);
|
||||
|
||||
expect(isDateOutsideRange({ day, fromDate })).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when day is identical to toDate", () => {
|
||||
const day = new Date(2023, 11, 31);
|
||||
const toDate = new Date(2023, 11, 31);
|
||||
|
||||
expect(isDateOutsideRange({ day, toDate })).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when day is identical to both fromDate and toDate", () => {
|
||||
const day = new Date(2023, 5, 15);
|
||||
const fromDate = new Date(2023, 5, 15);
|
||||
const toDate = new Date(2023, 5, 15);
|
||||
|
||||
expect(isDateOutsideRange({ day, fromDate, toDate })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { isAfter, isBefore, startOfMonth, startOfYear } from "date-fns";
|
||||
|
||||
/**
|
||||
* Makes sure the month is within the min and max daterange to avoid showing disabled months
|
||||
* @note We do not warn the user if start > end now
|
||||
*/
|
||||
const clampDisplayMonth = ({
|
||||
month,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
month?: Date;
|
||||
start?: Date;
|
||||
end?: Date;
|
||||
}): Date | undefined => {
|
||||
if (!month) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let monthToShow = month;
|
||||
|
||||
if (start && isBefore(monthToShow, start)) {
|
||||
monthToShow = start;
|
||||
}
|
||||
|
||||
if (end && isAfter(monthToShow, end)) {
|
||||
monthToShow = end;
|
||||
}
|
||||
|
||||
return startOfMonth(monthToShow);
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes sure the month is within the min and max daterange to avoid showing disabled months
|
||||
* @note We do not warn the user if start > end now
|
||||
*/
|
||||
const clampDisplayYear = ({
|
||||
month,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
month?: Date;
|
||||
start?: Date;
|
||||
end?: Date;
|
||||
}): Date | undefined => {
|
||||
if (!month) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let monthToShow = month;
|
||||
|
||||
if (start && monthToShow.getFullYear() < start.getFullYear()) {
|
||||
monthToShow = start;
|
||||
}
|
||||
|
||||
if (end && monthToShow.getFullYear() > end.getFullYear()) {
|
||||
monthToShow = end;
|
||||
}
|
||||
|
||||
return startOfYear(monthToShow);
|
||||
};
|
||||
|
||||
export { clampDisplayYear, clampDisplayMonth };
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { disableDate } from "..";
|
||||
import { disableDate } from "./dates-disabled";
|
||||
|
||||
describe("Returns if date should be disabled", () => {
|
||||
test("Should be disabled using Date (true)", () => {
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
import { isSameDay } from "date-fns";
|
||||
import { isDateRange } from "react-day-picker";
|
||||
import { isDateRange } from "../Date.typeutils";
|
||||
|
||||
// TODO: ((date: Date) => boolean)
|
||||
export const disableDate = (
|
||||
disabledSelection: Date | any[],
|
||||
date: Date,
|
||||
@@ -0,0 +1,143 @@
|
||||
import { nb } from "date-fns/locale";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { getMonthOptions, getYearOptions } from "./dropdown-options";
|
||||
|
||||
describe("getYearOptions", () => {
|
||||
test("should return undefined if navStart is undefined", () => {
|
||||
const result = getYearOptions(undefined, new Date(), nb);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should return undefined if navEnd is undefined", () => {
|
||||
const result = getYearOptions(new Date(), undefined, nb);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should return the correct year options within the interval", () => {
|
||||
const navStart = new Date(2020, 0, 1); // Januar 1, 2020
|
||||
const navEnd = new Date(2022, 11, 31); // Desember 31, 2022
|
||||
const result = getYearOptions(navStart, navEnd, nb);
|
||||
|
||||
const expected = [
|
||||
{ value: 2020, label: "2020", disabled: false },
|
||||
{ value: 2021, label: "2021", disabled: false },
|
||||
{ value: 2022, label: "2022", disabled: false },
|
||||
];
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test("should return the correct year options for a single year", () => {
|
||||
const navStart = new Date(2021, 0, 1); // Januar 1, 2021
|
||||
const navEnd = new Date(2021, 11, 31); // Desember 31, 2021
|
||||
const result = getYearOptions(navStart, navEnd, nb);
|
||||
|
||||
const expected = [{ value: 2021, label: "2021", disabled: false }];
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test("should return the correct year options when navStart and navEnd are the same date", () => {
|
||||
const navStart = new Date(2021, 0, 1); // Januar 1, 2021
|
||||
const navEnd = new Date(2021, 0, 1); // Januar 1, 2021
|
||||
const result = getYearOptions(navStart, navEnd, nb);
|
||||
|
||||
const expected = [{ value: 2021, label: "2021", disabled: false }];
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMonthOptions", () => {
|
||||
test("should return the correct month options for the given year", () => {
|
||||
const displayMonth = new Date(2021, 0, 1); // Januar 1, 2021
|
||||
const result = getMonthOptions(displayMonth, undefined, undefined, nb);
|
||||
|
||||
const expected = [
|
||||
{ value: 0, label: "januar", disabled: false },
|
||||
{ value: 1, label: "februar", disabled: false },
|
||||
{ value: 2, label: "mars", disabled: false },
|
||||
{ value: 3, label: "april", disabled: false },
|
||||
{ value: 4, label: "mai", disabled: false },
|
||||
{ value: 5, label: "juni", disabled: false },
|
||||
{ value: 6, label: "juli", disabled: false },
|
||||
{ value: 7, label: "august", disabled: false },
|
||||
{ value: 8, label: "september", disabled: false },
|
||||
{ value: 9, label: "oktober", disabled: false },
|
||||
{ value: 10, label: "november", disabled: false },
|
||||
{ value: 11, label: "desember", disabled: false },
|
||||
];
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test("should disable months before navStart", () => {
|
||||
const displayMonth = new Date(2021, 0, 1); // Januar 1, 2021
|
||||
const navStart = new Date(2021, 5, 1); // Juni 1, 2021
|
||||
const result = getMonthOptions(displayMonth, navStart, undefined, nb);
|
||||
|
||||
const expected = [
|
||||
{ value: 0, label: "januar", disabled: true },
|
||||
{ value: 1, label: "februar", disabled: true },
|
||||
{ value: 2, label: "mars", disabled: true },
|
||||
{ value: 3, label: "april", disabled: true },
|
||||
{ value: 4, label: "mai", disabled: true },
|
||||
{ value: 5, label: "juni", disabled: false },
|
||||
{ value: 6, label: "juli", disabled: false },
|
||||
{ value: 7, label: "august", disabled: false },
|
||||
{ value: 8, label: "september", disabled: false },
|
||||
{ value: 9, label: "oktober", disabled: false },
|
||||
{ value: 10, label: "november", disabled: false },
|
||||
{ value: 11, label: "desember", disabled: false },
|
||||
];
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test("should disable months after navEnd", () => {
|
||||
const displayMonth = new Date(2021, 0, 1); // Januar 1, 2021
|
||||
const navEnd = new Date(2021, 5, 1); // Juni 1, 2021
|
||||
const result = getMonthOptions(displayMonth, undefined, navEnd, nb);
|
||||
|
||||
const expected = [
|
||||
{ value: 0, label: "januar", disabled: false },
|
||||
{ value: 1, label: "februar", disabled: false },
|
||||
{ value: 2, label: "mars", disabled: false },
|
||||
{ value: 3, label: "april", disabled: false },
|
||||
{ value: 4, label: "mai", disabled: false },
|
||||
{ value: 5, label: "juni", disabled: false },
|
||||
{ value: 6, label: "juli", disabled: true },
|
||||
{ value: 7, label: "august", disabled: true },
|
||||
{ value: 8, label: "september", disabled: true },
|
||||
{ value: 9, label: "oktober", disabled: true },
|
||||
{ value: 10, label: "november", disabled: true },
|
||||
{ value: 11, label: "desember", disabled: true },
|
||||
];
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test("should disable months before navStart and after navEnd", () => {
|
||||
const displayMonth = new Date(2021, 0, 1); // Januar 1, 2021
|
||||
const navStart = new Date(2021, 3, 1); // April 1, 2021
|
||||
const navEnd = new Date(2021, 8, 1); // September 1, 2021
|
||||
const result = getMonthOptions(displayMonth, navStart, navEnd, nb);
|
||||
|
||||
const expected = [
|
||||
{ value: 0, label: "januar", disabled: true },
|
||||
{ value: 1, label: "februar", disabled: true },
|
||||
{ value: 2, label: "mars", disabled: true },
|
||||
{ value: 3, label: "april", disabled: false },
|
||||
{ value: 4, label: "mai", disabled: false },
|
||||
{ value: 5, label: "juni", disabled: false },
|
||||
{ value: 6, label: "juli", disabled: false },
|
||||
{ value: 7, label: "august", disabled: false },
|
||||
{ value: 8, label: "september", disabled: false },
|
||||
{ value: 9, label: "oktober", disabled: true },
|
||||
{ value: 10, label: "november", disabled: true },
|
||||
{ value: 11, label: "desember", disabled: true },
|
||||
];
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
Locale,
|
||||
addYears,
|
||||
eachMonthOfInterval,
|
||||
endOfYear,
|
||||
format,
|
||||
getMonth,
|
||||
getYear,
|
||||
isBefore,
|
||||
isSameYear,
|
||||
startOfMonth,
|
||||
startOfYear,
|
||||
} from "date-fns";
|
||||
|
||||
/** Return the months to show in the dropdown. */
|
||||
export function getMonthOptions(
|
||||
displayMonth: Date,
|
||||
navStart: Date | undefined,
|
||||
navEnd: Date | undefined,
|
||||
locale: Locale,
|
||||
):
|
||||
| {
|
||||
value: number;
|
||||
label: string;
|
||||
disabled: boolean;
|
||||
}[]
|
||||
| undefined {
|
||||
const months = eachMonthOfInterval({
|
||||
start: startOfYear(displayMonth),
|
||||
end: endOfYear(displayMonth),
|
||||
});
|
||||
|
||||
const options = months.map((month) => {
|
||||
const label = format(month, "LLLL", { locale });
|
||||
const value = getMonth(month);
|
||||
const disabled =
|
||||
(navStart && month < startOfMonth(navStart)) ||
|
||||
(navEnd && month > startOfMonth(navEnd)) ||
|
||||
false;
|
||||
return { value, label, disabled };
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/** Return the years to show in the dropdown. */
|
||||
export function getYearOptions(
|
||||
navStart: Date | undefined,
|
||||
navEnd: Date | undefined,
|
||||
locale: Locale,
|
||||
):
|
||||
| {
|
||||
value: number;
|
||||
label: string;
|
||||
disabled: boolean;
|
||||
}[]
|
||||
| undefined {
|
||||
if (!navStart) return undefined;
|
||||
if (!navEnd) return undefined;
|
||||
|
||||
const firstNavYear = startOfYear(navStart);
|
||||
const lastNavYear = endOfYear(navEnd);
|
||||
const years: Date[] = [];
|
||||
|
||||
let year = firstNavYear;
|
||||
while (isBefore(year, lastNavYear) || isSameYear(year, lastNavYear)) {
|
||||
years.push(year);
|
||||
year = addYears(year, 1);
|
||||
}
|
||||
|
||||
return years.map((_year) => {
|
||||
const label = format(_year, "yyyy", { locale });
|
||||
return {
|
||||
value: getYear(_year),
|
||||
label,
|
||||
disabled: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
File renamed without changes.
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { nb } from "date-fns/locale";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { formatDateForInput } from "../format-date";
|
||||
import { parseDate } from "../parse-date";
|
||||
import { formatDateForInput } from "./format-date";
|
||||
import { parseDate } from "./parse-date";
|
||||
|
||||
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date", false);
|
||||
const parseTwoDigit = (inp: string) =>
|
||||
@@ -0,0 +1,16 @@
|
||||
export { formatDateForInput } from "./format-date";
|
||||
export {
|
||||
INPUT_DATE_STRING_FORMAT_DATE,
|
||||
INPUT_DATE_STRING_FORMAT_MONTH,
|
||||
parseDate,
|
||||
} from "./parse-date";
|
||||
export { disableDate } from "./dates-disabled";
|
||||
export {
|
||||
dateIsInCurrentMonth,
|
||||
isValidDate,
|
||||
isDateOutsideRange,
|
||||
} from "./check-dates";
|
||||
export { isMatch, isDateInRange } from "./is-match";
|
||||
export { clampDisplayMonth, clampDisplayYear } from "./clamp-dates";
|
||||
export { getMonthOptions, getYearOptions } from "./dropdown-options";
|
||||
export { calendarRange } from "./calendar-range";
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { isMatch } from "..";
|
||||
import { isMatch } from "./is-match";
|
||||
|
||||
const disabled = [
|
||||
new Date("Sep 8 2022"),
|
||||
+2
-11
@@ -5,21 +5,12 @@ import {
|
||||
isSameMonth,
|
||||
} from "date-fns";
|
||||
import {
|
||||
DateAfter,
|
||||
DateBefore,
|
||||
DateRange,
|
||||
Matcher,
|
||||
isDateAfterType,
|
||||
isDateBeforeType,
|
||||
isDateRange,
|
||||
} from "react-day-picker";
|
||||
|
||||
export type Matcher =
|
||||
| ((date: Date) => boolean)
|
||||
| Date
|
||||
| Date[]
|
||||
| DateRange
|
||||
| DateBefore
|
||||
| DateAfter;
|
||||
} from "../Date.typeutils";
|
||||
|
||||
function isDateType(value: unknown): value is Date {
|
||||
return isDate(value);
|
||||
File renamed without changes.
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { getMonth } from "date-fns";
|
||||
import { nb } from "date-fns/locale";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { isValidDate, parseDate } from "..";
|
||||
import { isValidDate } from "./check-dates";
|
||||
import { parseDate } from "./parse-date";
|
||||
|
||||
const check = (inp: string) =>
|
||||
// eslint-disable-next-line @vitest/valid-expect
|
||||
+33
-71
@@ -1,14 +1,13 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import { expect, userEvent, within } from "@storybook/test";
|
||||
import { isSameDay } from "date-fns";
|
||||
import React, { useId, useState } from "react";
|
||||
import { Button } from "../../button";
|
||||
import { HGrid } from "../../layout/grid";
|
||||
import { VStack } from "../../layout/stack";
|
||||
import Modal from "../../modal/Modal";
|
||||
import { BodyLong } from "../../typography";
|
||||
import { useDatepicker, useRangeDatepicker } from "../hooks";
|
||||
import DatePicker, { DatePickerProps } from "./DatePicker";
|
||||
import { useDatepicker } from "./hooks/useDatepicker";
|
||||
import { useRangeDatepicker } from "./hooks/useRangeDatepicker";
|
||||
|
||||
const disabledDays = [
|
||||
new Date("Oct 10 2022"),
|
||||
@@ -147,14 +146,10 @@ export const DisabledDays = () => (
|
||||
/>
|
||||
);
|
||||
|
||||
export const ShowWeekNumber = () => (
|
||||
<DatePicker.Standalone showWeekNumber today={new Date("2006-07-01")} />
|
||||
);
|
||||
|
||||
export const UseDatepicker = () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
toDate: new Date("Feb 23 2024"),
|
||||
toDate: new Date("Feb 23 2029"),
|
||||
onDateChange: console.log,
|
||||
onValidate: console.log,
|
||||
});
|
||||
@@ -200,7 +195,10 @@ export const EN = () => (
|
||||
);
|
||||
|
||||
export const Standalone = () => (
|
||||
<DatePicker.Standalone today={new Date("2006-07-01")} />
|
||||
<DatePicker.Standalone
|
||||
today={new Date("2006-07-01")}
|
||||
onSelect={console.info}
|
||||
/>
|
||||
);
|
||||
|
||||
export const StandaloneRange = () => (
|
||||
@@ -298,22 +296,6 @@ export const UseRangedDatepickerValidation = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DefaultShownMonth = () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
onDateChange: console.log,
|
||||
defaultMonth: new Date("Oct 23 2022"),
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "1rem" }}>
|
||||
<DatePicker {...datepickerProps}>
|
||||
<DatePicker.Input {...inputProps} label="Velg dato" />
|
||||
</DatePicker>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Size = () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
@@ -399,7 +381,7 @@ export const StandaloneOptions = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const WeekDayClick = () => {
|
||||
export const WeekNumber = () => {
|
||||
const [days, setDays] = useState<Date[]>([]);
|
||||
|
||||
const handleWeekClick = (dates: Date[]) => {
|
||||
@@ -414,7 +396,8 @@ export const WeekDayClick = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack gap="8">
|
||||
<HGrid columns={{ xs: 1, md: 2 }} gap="space-80 space-16">
|
||||
<DatePicker.Standalone showWeekNumber today={new Date("2006-07-01")} />
|
||||
<DatePicker.Standalone
|
||||
showWeekNumber
|
||||
mode="multiple"
|
||||
@@ -422,6 +405,7 @@ export const WeekDayClick = () => {
|
||||
onSelect={(dates) => dates && setDays(dates)}
|
||||
selected={days}
|
||||
today={new Date("Nov 23 2022")}
|
||||
toDate={new Date("Nov 19 2023")}
|
||||
/>
|
||||
<DatePicker.Standalone
|
||||
showWeekNumber
|
||||
@@ -432,43 +416,29 @@ export const WeekDayClick = () => {
|
||||
today={new Date("Nov 23 2022")}
|
||||
disableWeekends
|
||||
/>
|
||||
</VStack>
|
||||
<DatePicker
|
||||
dropdownCaption
|
||||
showWeekNumber
|
||||
today={new Date("2006-07-01")}
|
||||
open
|
||||
>
|
||||
<DatePicker.Input label="Velg dato" />
|
||||
</DatePicker>
|
||||
<DatePicker
|
||||
dropdownCaption
|
||||
showWeekNumber
|
||||
today={new Date("2006-07-01")}
|
||||
open
|
||||
mode="multiple"
|
||||
onWeekNumberClick={(_, dates) => handleWeekClick(dates)}
|
||||
selected={days}
|
||||
>
|
||||
<DatePicker.Input label="Velg dato" />
|
||||
</DatePicker>
|
||||
</HGrid>
|
||||
);
|
||||
};
|
||||
|
||||
export const Required = {
|
||||
render: () => {
|
||||
const { datepickerProps } = useDatepicker({
|
||||
defaultSelected: new Date("Apr 10 2024"),
|
||||
required: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ height: "20rem" }}>
|
||||
<DatePicker.Standalone {...datepickerProps} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const button10 = canvas.getByRole("button", { pressed: true });
|
||||
|
||||
await userEvent.click(button10);
|
||||
|
||||
expect(button10.ariaPressed).toBe("true");
|
||||
|
||||
const button17 = canvas.getByText("17").closest("button");
|
||||
|
||||
expect(button17?.ariaPressed).toBe("false");
|
||||
|
||||
button17 && (await userEvent.click(button17));
|
||||
|
||||
expect(button17?.ariaPressed).toBe("true");
|
||||
expect(button10.ariaPressed).toBe("false");
|
||||
},
|
||||
};
|
||||
|
||||
export const ModalDemo = () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
@@ -520,10 +490,6 @@ export const Chromatic: Story = {
|
||||
<h2>DisabledDays</h2>
|
||||
<DisabledDays />
|
||||
</div>
|
||||
<div>
|
||||
<h2>ShowWeekNumber</h2>
|
||||
<ShowWeekNumber />
|
||||
</div>
|
||||
<div>
|
||||
<h2>UseDatepicker</h2>
|
||||
<UseDatepicker />
|
||||
@@ -576,10 +542,6 @@ export const Chromatic: Story = {
|
||||
<h2>UseRangedDatepickerValidation</h2>
|
||||
<UseRangedDatepickerValidation />
|
||||
</div>
|
||||
<div>
|
||||
<h2>DefaultShownMonth</h2>
|
||||
<DefaultShownMonth />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Size</h2>
|
||||
<Size />
|
||||
@@ -593,8 +555,8 @@ export const Chromatic: Story = {
|
||||
<StandaloneOptions />
|
||||
</div>
|
||||
<div>
|
||||
<h2>WeekDayClick</h2>
|
||||
<WeekDayClick />
|
||||
<h2>WeekNumber</h2>
|
||||
<WeekNumber />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
@@ -0,0 +1,416 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import { expect, userEvent, within } from "@storybook/test";
|
||||
import { addMonths, format } from "date-fns";
|
||||
import { nb } from "date-fns/locale";
|
||||
import React from "react";
|
||||
import DatePicker from "./DatePicker";
|
||||
import { useDatepicker } from "./hooks/useDatepicker";
|
||||
|
||||
export default {
|
||||
title: "ds-react/DatePicker/Tests",
|
||||
component: DatePicker,
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
} satisfies Meta<typeof DatePicker>;
|
||||
|
||||
type Story = StoryObj<typeof DatePicker>;
|
||||
|
||||
const currentDate = new Date();
|
||||
const previousMonth = addMonths(currentDate, -1);
|
||||
const nextMonth = addMonths(currentDate, 1);
|
||||
|
||||
/**
|
||||
* Validate that the monthpicker shows the current year by default
|
||||
* and that the navigation buttons work as expected
|
||||
*/
|
||||
export const LabelCaptionNavigation: Story = {
|
||||
render: () => <DatePicker.Standalone />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const label = canvas.getByText(
|
||||
format(currentDate, "LLLL y", { locale: nb }),
|
||||
);
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
|
||||
const todayButton = canvas.getByRole("button", {
|
||||
name: format(currentDate, "cccc d", {
|
||||
locale: nb,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(todayButton.getAttribute("data-today")).toBe("true");
|
||||
|
||||
await userEvent.tab();
|
||||
await userEvent.keyboard("{Enter}");
|
||||
|
||||
expect(
|
||||
canvas.getByText(format(previousMonth, "LLLL y", { locale: nb })),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await userEvent.tab();
|
||||
await userEvent.keyboard("{Enter}");
|
||||
await userEvent.keyboard("{Enter}");
|
||||
|
||||
expect(
|
||||
canvas.getByText(format(nextMonth, "LLLL y", { locale: nb })),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that the monthpicker shows the current year by default
|
||||
* and that the navigation selects work as expected
|
||||
*/
|
||||
export const DropdownCaptionNavigation: Story = {
|
||||
render: () => (
|
||||
<DatePicker.Standalone
|
||||
dropdownCaption
|
||||
fromDate={new Date("05 May 2021")}
|
||||
toDate={new Date("05 Apr 2029")}
|
||||
defaultMonth={new Date("03 Jan 2025")}
|
||||
/>
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
let dropdown = canvas.getByDisplayValue("2025");
|
||||
|
||||
await userEvent.selectOptions(dropdown, "2021");
|
||||
|
||||
expect(
|
||||
canvas.getByRole("button", {
|
||||
name: format(new Date("04 May 2021"), "cccc d", { locale: nb }),
|
||||
}),
|
||||
).toBeDisabled();
|
||||
|
||||
dropdown = canvas.getByDisplayValue("2021");
|
||||
|
||||
await userEvent.selectOptions(dropdown, "2029");
|
||||
|
||||
expect(
|
||||
canvas.getByRole("button", {
|
||||
name: format(new Date("06 Apr 2029"), "cccc d", { locale: nb }),
|
||||
}),
|
||||
).toBeDisabled();
|
||||
},
|
||||
};
|
||||
|
||||
export const Required: Story = {
|
||||
render: () => {
|
||||
const { datepickerProps } = useDatepicker({
|
||||
defaultSelected: new Date("Apr 10 2024"),
|
||||
required: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ height: "20rem" }}>
|
||||
<DatePicker.Standalone {...datepickerProps} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const button10 = canvas.getByRole("button", { pressed: true });
|
||||
|
||||
await userEvent.click(button10);
|
||||
|
||||
expect(button10.ariaPressed).toBe("true");
|
||||
|
||||
const button17 = canvas.getByText("17").closest("button");
|
||||
|
||||
expect(button17?.ariaPressed).toBe("false");
|
||||
|
||||
if (button17) {
|
||||
await userEvent.click(button17);
|
||||
}
|
||||
|
||||
expect(canvas.getByText("17").closest("button")?.ariaPressed).toBe("true");
|
||||
expect(canvas.getByText("10").closest("button")?.ariaPressed).toBe("false");
|
||||
},
|
||||
};
|
||||
|
||||
export const DomStructure: Story = {
|
||||
render: () => (
|
||||
<DatePicker.Standalone
|
||||
data-testid="date-table"
|
||||
selected={new Date("3 Jan 2025")}
|
||||
defaultMonth={new Date("03 Jan 2025")}
|
||||
/>
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const picker = canvas.getByTestId("date-table");
|
||||
|
||||
expect(picker.className).toEqual("rdp navds-date");
|
||||
|
||||
const months = picker.firstChild;
|
||||
expect(months).toBeInTheDocument();
|
||||
|
||||
if (months) {
|
||||
expect(
|
||||
(months as HTMLDivElement).className.startsWith("rdp-months"),
|
||||
).toBeTruthy();
|
||||
expect(months.nodeName).toBe("DIV");
|
||||
}
|
||||
|
||||
const month = months?.firstChild;
|
||||
expect(month).toBeInTheDocument();
|
||||
|
||||
if (month) {
|
||||
expect(
|
||||
(month as HTMLDivElement).className.startsWith("rdp-month"),
|
||||
).toBeTruthy();
|
||||
expect(month.nodeName).toBe("DIV");
|
||||
}
|
||||
|
||||
const table = month?.lastChild;
|
||||
expect(table).toBeInTheDocument();
|
||||
|
||||
if (table) {
|
||||
const typedTable = table as HTMLTableElement;
|
||||
expect(typedTable.className.startsWith("rdp-table")).toBeTruthy();
|
||||
expect((table as HTMLTableElement).nodeName).toBe("TABLE");
|
||||
|
||||
expect(typedTable.role).toEqual("grid");
|
||||
}
|
||||
|
||||
const tHead = table?.firstChild;
|
||||
expect(tHead).toBeInTheDocument();
|
||||
|
||||
if (tHead) {
|
||||
expect(tHead.nodeName).toBe("THEAD");
|
||||
expect((tHead as HTMLElement).className).toEqual("rdp-head");
|
||||
expect((tHead as HTMLElement).ariaHidden).toBe("true");
|
||||
}
|
||||
|
||||
const tBody = table?.lastChild;
|
||||
expect(tBody).toBeInTheDocument();
|
||||
|
||||
if (tBody) {
|
||||
expect(tBody.nodeName).toBe("TBODY");
|
||||
expect((tBody as HTMLElement).className).toEqual("rdp-tbody");
|
||||
}
|
||||
|
||||
const row = tBody?.firstChild;
|
||||
expect(row).toBeInTheDocument();
|
||||
|
||||
if (row) {
|
||||
expect(row.nodeName).toBe("TR");
|
||||
expect((row as HTMLElement).className).toEqual("rdp-row");
|
||||
}
|
||||
|
||||
const cells = row?.childNodes;
|
||||
|
||||
/**
|
||||
* Since default selected date is 3 Jan 2025:
|
||||
* - node 0 and 1 is "invisible"
|
||||
* - node 2 and 3 is unselected
|
||||
* - node 4 is selected
|
||||
* - node 5 and 6 is unselected
|
||||
*/
|
||||
if (cells) {
|
||||
expect(cells.length).toBe(7);
|
||||
|
||||
cells.forEach((cell, index) => {
|
||||
expect(cell.nodeName).toBe("TD");
|
||||
expect((cell as HTMLElement).className).toEqual("rdp-cell");
|
||||
|
||||
const button = cell.firstChild;
|
||||
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
if (button) {
|
||||
const typedButton = button as HTMLButtonElement;
|
||||
expect(button.nodeName).toBe("BUTTON");
|
||||
expect(typedButton.type).toBe("button");
|
||||
expect(typedButton.classList.contains("rdp-button")).toBeTruthy();
|
||||
expect(typedButton.ariaLabel).toBeTruthy();
|
||||
|
||||
if (index <= 1) {
|
||||
expect(typedButton.ariaHidden).toBe("true");
|
||||
}
|
||||
|
||||
if (index !== 4) {
|
||||
expect(typedButton.ariaPressed).toBe("false");
|
||||
expect(typedButton.tabIndex).toBe(-1);
|
||||
} else {
|
||||
expect(
|
||||
typedButton.classList.contains("rdp-day_selected"),
|
||||
).toBeTruthy();
|
||||
expect(typedButton.ariaPressed).toBe("true");
|
||||
expect(typedButton.tabIndex).toBe(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const oldDate = new Date("Oct 23 2022");
|
||||
|
||||
export const HookDefaultMonth: Story = {
|
||||
render: () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
onDateChange: console.log,
|
||||
defaultMonth: oldDate,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "1rem" }}>
|
||||
<DatePicker {...datepickerProps}>
|
||||
<DatePicker.Input {...inputProps} label="Velg dato" />
|
||||
</DatePicker>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const button = canvas.getByLabelText("Åpne datovelger");
|
||||
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(button);
|
||||
const dialog = canvas.getByRole("dialog");
|
||||
|
||||
expect(dialog).toBeVisible();
|
||||
expect(dialog.ariaHidden).toBe("false");
|
||||
|
||||
const label = within(dialog).getByText(
|
||||
format(oldDate, "LLLL y", { locale: nb }),
|
||||
{},
|
||||
);
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const HookDefaultMonthWhenSelected: Story = {
|
||||
render: () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
onDateChange: console.log,
|
||||
defaultSelected: oldDate,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "1rem" }}>
|
||||
<DatePicker {...datepickerProps}>
|
||||
<DatePicker.Input {...inputProps} label="Velg dato" />
|
||||
</DatePicker>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const button = canvas.getByLabelText("Åpne datovelger");
|
||||
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(button);
|
||||
const dialog = canvas.getByRole("dialog");
|
||||
|
||||
expect(dialog).toBeVisible();
|
||||
expect(dialog.ariaHidden).toBe("false");
|
||||
|
||||
const label = within(dialog).getByText(
|
||||
format(oldDate, "LLLL y", { locale: nb }),
|
||||
{},
|
||||
);
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
|
||||
const selectedButton = within(dialog).getByRole("button", {
|
||||
pressed: true,
|
||||
});
|
||||
|
||||
expect(selectedButton.ariaLabel).toEqual("søndag 23");
|
||||
},
|
||||
};
|
||||
|
||||
const fallbackToFromDate = new Date("Aug 23 2019");
|
||||
|
||||
export const HookFallbackToFromDate: Story = {
|
||||
render: () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
fromDate: fallbackToFromDate,
|
||||
onDateChange: console.log,
|
||||
defaultMonth: new Date("Oct 23 2010"),
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "1rem" }}>
|
||||
<DatePicker {...datepickerProps}>
|
||||
<DatePicker.Input {...inputProps} label="Velg dato" />
|
||||
</DatePicker>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const button = canvas.getByLabelText("Åpne datovelger");
|
||||
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(button);
|
||||
const dialog = canvas.getByRole("dialog");
|
||||
|
||||
expect(dialog).toBeVisible();
|
||||
expect(dialog.ariaHidden).toBe("false");
|
||||
|
||||
const label = within(dialog).getByText(
|
||||
format(fallbackToFromDate, "LLLL y", { locale: nb }),
|
||||
{},
|
||||
);
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
const fallbackToToDate = new Date("Aug 23 2028");
|
||||
|
||||
export const HookFallbackToDate: Story = {
|
||||
render: () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
toDate: fallbackToToDate,
|
||||
onDateChange: console.log,
|
||||
defaultMonth: new Date("Oct 23 2030"),
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "1rem" }}>
|
||||
<DatePicker {...datepickerProps}>
|
||||
<DatePicker.Input {...inputProps} label="Velg dato" />
|
||||
</DatePicker>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const button = canvas.getByLabelText("Åpne datovelger");
|
||||
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(button);
|
||||
const dialog = canvas.getByRole("dialog");
|
||||
|
||||
expect(dialog).toBeVisible();
|
||||
expect(dialog.ariaHidden).toBe("false");
|
||||
|
||||
const label = within(dialog).getByText(
|
||||
format(fallbackToToDate, "LLLL y", { locale: nb }),
|
||||
{},
|
||||
);
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
@@ -1,23 +1,23 @@
|
||||
import cl from "clsx";
|
||||
import { isWeekend } from "date-fns";
|
||||
import { isSameDay } from "date-fns";
|
||||
import React, { forwardRef, useState } from "react";
|
||||
import { DateRange, DayPicker, isMatch } from "react-day-picker";
|
||||
import { omit } from "../../util";
|
||||
import { useId } from "../../util/hooks";
|
||||
import { DateRange } from "react-day-picker";
|
||||
import { useControllableState, useId } from "../../util/hooks";
|
||||
import { useMergeRefs } from "../../util/hooks/useMergeRefs";
|
||||
import { useDateLocale, useI18n } from "../../util/i18n/i18n.hooks";
|
||||
import { DateInputContext, DateTranslationContextProvider } from "../context";
|
||||
import { DatePickerInput } from "../parts/DateInput";
|
||||
import { DateWrapper } from "../parts/DateWrapper";
|
||||
import { getLocaleFromString, getTranslations } from "../utils";
|
||||
import DatePickerStandalone from "./DatePickerStandalone";
|
||||
import Caption from "./parts/Caption";
|
||||
import DropdownCaption from "./parts/DropdownCaption";
|
||||
import { HeadRow } from "./parts/HeadRow";
|
||||
import Row from "./parts/Row";
|
||||
import TableHead from "./parts/TableHead";
|
||||
import WeekNumber from "./parts/WeekNumber";
|
||||
import { ConditionalModeProps, DatePickerDefaultProps } from "./types";
|
||||
import { useI18n } from "../../util/i18n/i18n.hooks";
|
||||
import { DateDialog } from "../Date.Dialog";
|
||||
import { DateInputContextProvider, DatePickerInput } from "../Date.Input";
|
||||
import {
|
||||
DateTranslationContextProvider,
|
||||
getTranslations,
|
||||
} from "../Date.locale";
|
||||
import { isDateRange } from "../Date.typeutils";
|
||||
import {
|
||||
ConditionalModeProps,
|
||||
DatePickerDefaultProps,
|
||||
} from "./DatePicker.types";
|
||||
import { ReactDayPicker } from "./parts/DatePicker.RDP";
|
||||
import DatePickerStandalone from "./parts/DatePicker.Standalone";
|
||||
|
||||
export type DatePickerProps = DatePickerDefaultProps & ConditionalModeProps;
|
||||
|
||||
@@ -69,20 +69,15 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
|
||||
children,
|
||||
locale,
|
||||
translations,
|
||||
dropdownCaption,
|
||||
disabled = [],
|
||||
disableWeekends = false,
|
||||
showWeekNumber = false,
|
||||
selected,
|
||||
id,
|
||||
defaultSelected,
|
||||
className,
|
||||
wrapperClassName,
|
||||
open: _open,
|
||||
onClose,
|
||||
onOpenToggle,
|
||||
strategy,
|
||||
onWeekNumberClick,
|
||||
mode,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
@@ -92,104 +87,84 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
|
||||
translations,
|
||||
getTranslations(locale),
|
||||
);
|
||||
const langProviderLocale = useDateLocale();
|
||||
const ariaId = useId(id);
|
||||
const [open, setOpen] = useState(_open ?? false);
|
||||
|
||||
const [open, setOpen] = useControllableState({
|
||||
defaultValue: false,
|
||||
value: _open,
|
||||
});
|
||||
|
||||
/* We use state here to insure that anchor is defined if open is true on initial render */
|
||||
const [wrapperRef, setWrapperRef] = useState<HTMLDivElement | null>(null);
|
||||
const mergedRef = useMergeRefs(setWrapperRef, ref);
|
||||
|
||||
const [selectedDates, setSelectedDates] = React.useState<
|
||||
const [value, setValue] = useControllableState<
|
||||
Date | Date[] | DateRange | undefined
|
||||
>(defaultSelected);
|
||||
>({
|
||||
defaultValue: defaultSelected,
|
||||
value: selected,
|
||||
onChange: (newValue) => {
|
||||
let closeDialog = false;
|
||||
if (mode === "single" && newValue) {
|
||||
closeDialog = true;
|
||||
} else if (isDateRange(newValue) && newValue.from && newValue.to) {
|
||||
closeDialog = true;
|
||||
|
||||
const mode = rest.mode ?? ("single" as any);
|
||||
if (isSameDay(newValue.from, newValue.to)) {
|
||||
closeDialog = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param newSelected Date | Date[] | DateRange | undefined
|
||||
*/
|
||||
const handleSelect = (newSelected) => {
|
||||
setSelectedDates(newSelected);
|
||||
if (closeDialog) {
|
||||
onClose?.();
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
if (rest.mode === "single") {
|
||||
newSelected && (onClose?.() ?? setOpen(false));
|
||||
} else if (rest.mode === "range") {
|
||||
newSelected?.from && newSelected?.to && (onClose?.() ?? setOpen(false));
|
||||
}
|
||||
rest?.onSelect?.(newSelected);
|
||||
};
|
||||
|
||||
const DatePickerComponent = (
|
||||
<DayPicker
|
||||
locale={locale ? getLocaleFromString(locale) : langProviderLocale}
|
||||
mode={mode}
|
||||
onSelect={handleSelect}
|
||||
selected={selected ?? selectedDates}
|
||||
components={{
|
||||
Caption: dropdownCaption ? DropdownCaption : Caption,
|
||||
Head: TableHead,
|
||||
HeadRow,
|
||||
WeekNumber,
|
||||
Row,
|
||||
}}
|
||||
className={cl("navds-date", className)}
|
||||
classNames={{
|
||||
vhidden: "navds-sr-only",
|
||||
}}
|
||||
disabled={(day) => {
|
||||
return (disableWeekends && isWeekend(day)) || isMatch(day, disabled);
|
||||
}}
|
||||
weekStartsOn={1}
|
||||
initialFocus={false}
|
||||
modifiers={{
|
||||
weekend: (day) => disableWeekends && isWeekend(day),
|
||||
}}
|
||||
modifiersClassNames={{
|
||||
weekend: "rdp-day__weekend",
|
||||
}}
|
||||
showWeekNumber={showWeekNumber}
|
||||
onWeekNumberClick={mode === "multiple" ? onWeekNumberClick : undefined}
|
||||
fixedWeeks
|
||||
showOutsideDays
|
||||
{...omit(rest, ["onSelect"])}
|
||||
/>
|
||||
);
|
||||
rest?.onSelect?.(newValue as any);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<DateTranslationContextProvider translate={translate}>
|
||||
<DateInputContext.Provider
|
||||
value={{
|
||||
open: _open ?? open,
|
||||
onOpen: () => {
|
||||
setOpen((x) => !x);
|
||||
onOpenToggle?.();
|
||||
},
|
||||
ariaId,
|
||||
defined: true,
|
||||
<DateInputContextProvider
|
||||
open={open}
|
||||
onOpen={() => {
|
||||
setOpen((x) => !x);
|
||||
onOpenToggle?.();
|
||||
}}
|
||||
ariaId={ariaId}
|
||||
defined={true}
|
||||
>
|
||||
<div
|
||||
ref={mergedRef}
|
||||
className={cl("navds-date__wrapper", wrapperClassName)}
|
||||
>
|
||||
{children}
|
||||
<DateWrapper
|
||||
open={_open ?? open}
|
||||
<DateDialog
|
||||
open={open}
|
||||
anchor={wrapperRef}
|
||||
onClose={() => onClose?.() ?? setOpen(false)}
|
||||
onClose={() => {
|
||||
onClose?.();
|
||||
open && setOpen(false);
|
||||
}}
|
||||
locale={locale}
|
||||
translate={translate}
|
||||
variant={mode}
|
||||
variant={mode ?? "single"}
|
||||
popoverProps={{
|
||||
id: ariaId,
|
||||
strategy,
|
||||
}}
|
||||
>
|
||||
{DatePickerComponent}
|
||||
</DateWrapper>
|
||||
<ReactDayPicker
|
||||
{...rest}
|
||||
locale={locale}
|
||||
handleSelect={setValue}
|
||||
selected={value as any}
|
||||
mode={mode as any}
|
||||
/>
|
||||
</DateDialog>
|
||||
</div>
|
||||
</DateInputContext.Provider>
|
||||
</DateInputContextProvider>
|
||||
</DateTranslationContextProvider>
|
||||
);
|
||||
},
|
||||
|
||||
+10
-4
@@ -1,5 +1,6 @@
|
||||
import { DateRange, DayPickerBase, Matcher } from "react-day-picker";
|
||||
import { CalendarWeek, Matcher, PropsBase } from "react-day-picker";
|
||||
import { ComponentTranslation } from "../../util/i18n/i18n.types";
|
||||
import { DateRange } from "../Date.typeutils";
|
||||
|
||||
export type SingleMode = {
|
||||
mode?: "single";
|
||||
@@ -18,8 +19,10 @@ export type MultipleMode = {
|
||||
max?: number;
|
||||
/**
|
||||
* Allows selecting a week at a time. Only used with `mode` is set to "multiple".
|
||||
* @param week Current week number
|
||||
* @param days Dates in the week
|
||||
*/
|
||||
onWeekNumberClick?: DayPickerBase["onWeekNumberClick"];
|
||||
onWeekNumberClick?: (week: CalendarWeek["weekNumber"], days: Date[]) => void;
|
||||
};
|
||||
|
||||
export type RangeMode = {
|
||||
@@ -34,10 +37,13 @@ export type RangeMode = {
|
||||
|
||||
export type ConditionalModeProps = SingleMode | MultipleMode | RangeMode;
|
||||
|
||||
//github.com/gpbl/react-day-picker/blob/50b6dba/packages/react-day-picker/src/types/DayPickerBase.ts#L139
|
||||
// https://daypicker.dev/api/interfaces/PropsBase
|
||||
export interface DatePickerDefaultProps
|
||||
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect">,
|
||||
Pick<DayPickerBase, "month" | "onMonthChange" | "today" | "onDayClick"> {
|
||||
Pick<
|
||||
PropsBase,
|
||||
"month" | "onMonthChange" | "today" | "onDayClick" | "defaultMonth"
|
||||
> {
|
||||
/**
|
||||
* Element datepicker anchors to. Use <DatePicker.Input /> for built-in toggle, or make your own with the open/onClose props
|
||||
*/
|
||||
@@ -1,136 +0,0 @@
|
||||
import cl from "clsx";
|
||||
import { isWeekend } from "date-fns";
|
||||
import React, { forwardRef } from "react";
|
||||
import { DateRange, DayPicker, isMatch } from "react-day-picker";
|
||||
import { omit } from "../../util";
|
||||
import { useDateLocale, useI18n } from "../../util/i18n/i18n.hooks";
|
||||
import { DateTranslationContextProvider } from "../context";
|
||||
import { getLocaleFromString, getTranslations } from "../utils";
|
||||
import Caption from "./parts/Caption";
|
||||
import DropdownCaption from "./parts/DropdownCaption";
|
||||
import { HeadRow } from "./parts/HeadRow";
|
||||
import Row from "./parts/Row";
|
||||
import TableHead from "./parts/TableHead";
|
||||
import WeekNumber from "./parts/WeekNumber";
|
||||
import {
|
||||
DatePickerDefaultProps,
|
||||
MultipleMode,
|
||||
RangeMode,
|
||||
SingleMode,
|
||||
} from "./types";
|
||||
|
||||
interface DatePickerStandaloneDefaultProps
|
||||
extends Omit<
|
||||
DatePickerDefaultProps,
|
||||
"open" | "onClose" | "onOpenToggle" | "wrapperClassName" | "strategy"
|
||||
> {
|
||||
/**
|
||||
* Datepicker classname
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* If datepicker should be fixed to 6 weeks, regardless of actual weeks in month
|
||||
* @default true
|
||||
*/
|
||||
fixedWeeks?: boolean;
|
||||
}
|
||||
|
||||
type StandaloneConditionalModeProps = SingleMode | MultipleMode | RangeMode;
|
||||
|
||||
export type DatePickerStandaloneProps = DatePickerStandaloneDefaultProps &
|
||||
StandaloneConditionalModeProps;
|
||||
|
||||
export type DatePickerStandaloneType = React.ForwardRefExoticComponent<
|
||||
DatePickerStandaloneProps & React.RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
export const DatePickerStandalone: DatePickerStandaloneType = forwardRef<
|
||||
HTMLDivElement,
|
||||
DatePickerStandaloneProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
locale,
|
||||
translations,
|
||||
dropdownCaption,
|
||||
disabled = [],
|
||||
disableWeekends = false,
|
||||
showWeekNumber = false,
|
||||
selected,
|
||||
defaultSelected,
|
||||
onSelect,
|
||||
fixedWeeks = false,
|
||||
onWeekNumberClick,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const translate = useI18n(
|
||||
"DatePicker",
|
||||
translations,
|
||||
getTranslations(locale),
|
||||
);
|
||||
const langProviderLocale = useDateLocale();
|
||||
const [selectedDates, setSelectedDates] = React.useState<
|
||||
Date | Date[] | DateRange | undefined
|
||||
>(defaultSelected);
|
||||
|
||||
const mode = rest.mode ?? ("single" as any);
|
||||
|
||||
/**
|
||||
* @param newSelected Date | Date[] | DateRange | undefined
|
||||
*/
|
||||
const handleSelect = (newSelected) => {
|
||||
setSelectedDates(newSelected);
|
||||
onSelect?.(newSelected);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cl("navds-date__standalone-wrapper", className)}
|
||||
>
|
||||
<DateTranslationContextProvider translate={translate}>
|
||||
<DayPicker
|
||||
locale={locale ? getLocaleFromString(locale) : langProviderLocale}
|
||||
mode={mode}
|
||||
onSelect={handleSelect}
|
||||
selected={selected ?? selectedDates}
|
||||
components={{
|
||||
Caption: dropdownCaption ? DropdownCaption : Caption,
|
||||
Head: TableHead,
|
||||
HeadRow,
|
||||
WeekNumber,
|
||||
Row,
|
||||
}}
|
||||
className="navds-date"
|
||||
classNames={{ vhidden: "navds-sr-only" }}
|
||||
disabled={(day) => {
|
||||
return (
|
||||
(disableWeekends && isWeekend(day)) || isMatch(day, disabled)
|
||||
);
|
||||
}}
|
||||
weekStartsOn={1}
|
||||
initialFocus={false}
|
||||
modifiers={{
|
||||
weekend: (day) => disableWeekends && isWeekend(day),
|
||||
}}
|
||||
modifiersClassNames={{
|
||||
weekend: "rdp-day__weekend",
|
||||
}}
|
||||
showWeekNumber={showWeekNumber}
|
||||
onWeekNumberClick={
|
||||
mode === "multiple" ? onWeekNumberClick : undefined
|
||||
}
|
||||
fixedWeeks={fixedWeeks}
|
||||
showOutsideDays
|
||||
{...omit(rest, ["children", "id"])}
|
||||
/>
|
||||
</DateTranslationContextProvider>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default DatePickerStandalone;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { describe, test } from "vitest";
|
||||
import { useDatepicker } from "../hooks";
|
||||
import DatePicker from "./DatePicker";
|
||||
|
||||
const App = () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
onDateChange: console.info,
|
||||
});
|
||||
|
||||
return (
|
||||
<DatePicker {...datepickerProps}>
|
||||
<DatePicker.Input {...inputProps} label="Velg dato" />
|
||||
</DatePicker>
|
||||
);
|
||||
};
|
||||
|
||||
describe("Render datepicker", () => {
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
test("Should not crash when e.target is window", async () => {
|
||||
render(<App />);
|
||||
|
||||
await userEvent.click(screen.getByText("Velg dato"));
|
||||
});
|
||||
});
|
||||
+12
-13
@@ -1,15 +1,11 @@
|
||||
import { differenceInCalendarDays, isWeekend } from "date-fns";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { DayClickEventHandler, isMatch } from "react-day-picker";
|
||||
import { useDateLocale } from "../../util/i18n/i18n.hooks";
|
||||
import { DatePickerProps } from "../datepicker/DatePicker";
|
||||
import { DateInputProps } from "../parts/DateInput";
|
||||
import {
|
||||
formatDateForInput,
|
||||
getLocaleFromString,
|
||||
isValidDate,
|
||||
parseDate,
|
||||
} from "../utils";
|
||||
import { DayEventHandler, dateMatchModifiers } from "react-day-picker";
|
||||
import { useDateLocale } from "../../../util/i18n/i18n.hooks";
|
||||
import { DateInputProps } from "../../Date.Input";
|
||||
import { getLocaleFromString } from "../../Date.locale";
|
||||
import { formatDateForInput, isValidDate, parseDate } from "../../date-utils";
|
||||
import { DatePickerProps } from "../DatePicker";
|
||||
|
||||
export interface UseDatepickerOptions
|
||||
extends Pick<
|
||||
@@ -230,7 +226,10 @@ export const useDatepicker = (
|
||||
};
|
||||
|
||||
/* Only allow de-selecting if not required */
|
||||
const handleDayClick: DayClickEventHandler = (day, { selected }) => {
|
||||
const handleDayClick: DayEventHandler<React.MouseEvent> = (
|
||||
day,
|
||||
{ selected },
|
||||
) => {
|
||||
if (selected && required) {
|
||||
return;
|
||||
}
|
||||
@@ -274,13 +273,13 @@ export const useDatepicker = (
|
||||
if (
|
||||
!isValidDate(day) ||
|
||||
(disableWeekends && isWeekend(day)) ||
|
||||
(disabled && isMatch(day, disabled))
|
||||
(disabled && dateMatchModifiers(day, disabled))
|
||||
) {
|
||||
updateDate(undefined);
|
||||
updateValidation({
|
||||
isInvalid: !isValidDate(day),
|
||||
isWeekend: disableWeekends && isWeekend(day),
|
||||
isDisabled: disabled && isMatch(day, disabled),
|
||||
isDisabled: disabled && dateMatchModifiers(day, disabled),
|
||||
isValidDate: false,
|
||||
isEmpty: !e.target.value,
|
||||
isBefore: isBefore ?? false,
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { DatePicker } from "../datepicker";
|
||||
import { DatePicker } from "../DatePicker";
|
||||
import { useRangeDatepicker } from "./useRangeDatepicker";
|
||||
|
||||
const RangeDemo = () => {
|
||||
+19
-19
@@ -1,19 +1,17 @@
|
||||
import {
|
||||
isBefore as checkIsBefore,
|
||||
differenceInCalendarDays,
|
||||
isSameDay,
|
||||
isWeekend,
|
||||
} from "date-fns";
|
||||
import React, { useState } from "react";
|
||||
import { DateRange, isMatch } from "react-day-picker";
|
||||
import { useDateLocale } from "../../util/i18n/i18n.hooks";
|
||||
import { DatePickerProps } from "../datepicker/DatePicker";
|
||||
import { DateInputProps } from "../parts/DateInput";
|
||||
import {
|
||||
formatDateForInput,
|
||||
getLocaleFromString,
|
||||
isValidDate,
|
||||
parseDate,
|
||||
} from "../utils";
|
||||
import { dateMatchModifiers } from "react-day-picker";
|
||||
import { useDateLocale } from "../../../util/i18n/i18n.hooks";
|
||||
import { DateInputProps } from "../../Date.Input";
|
||||
import { getLocaleFromString } from "../../Date.locale";
|
||||
import { DateRange } from "../../Date.typeutils";
|
||||
import { formatDateForInput, isValidDate, parseDate } from "../../date-utils";
|
||||
import { DatePickerProps } from "../DatePicker";
|
||||
import { DateValidationT, UseDatepickerOptions } from "./useDatepicker";
|
||||
|
||||
export type RangeValidationT = {
|
||||
@@ -128,13 +126,13 @@ const fromValidation = (day: Date, opt?: UseRangeDatepickerOptions) => {
|
||||
if (
|
||||
isValidDate(day) &&
|
||||
!(opt?.disableWeekends && isWeekend(day)) &&
|
||||
!(opt?.disabled && isMatch(day, opt.disabled))
|
||||
!(opt?.disabled && dateMatchModifiers(day, opt.disabled))
|
||||
) {
|
||||
return {
|
||||
isValidDate: false,
|
||||
isInvalid: !isValidDate(day),
|
||||
isWeekend: opt?.disableWeekends && isWeekend(day),
|
||||
isDisabled: opt?.disabled && isMatch(day, opt.disabled),
|
||||
isDisabled: opt?.disabled && dateMatchModifiers(day, opt.disabled),
|
||||
isBefore,
|
||||
isAfter,
|
||||
};
|
||||
@@ -164,13 +162,13 @@ const toValidation = (
|
||||
if (
|
||||
isValidDate(day) &&
|
||||
!(opt?.disableWeekends && isWeekend(day)) &&
|
||||
!(opt?.disabled && isMatch(day, opt.disabled))
|
||||
!(opt?.disabled && dateMatchModifiers(day, opt.disabled))
|
||||
) {
|
||||
return {
|
||||
isValidDate: false,
|
||||
isInvalid: !isValidDate(day),
|
||||
isWeekend: opt?.disableWeekends && isWeekend(day),
|
||||
isDisabled: opt?.disabled && isMatch(day, opt.disabled),
|
||||
isDisabled: opt?.disabled && dateMatchModifiers(day, opt.disabled),
|
||||
isBefore,
|
||||
isAfter,
|
||||
isBeforeFrom,
|
||||
@@ -369,14 +367,16 @@ export const useRangeDatepicker = (
|
||||
return (
|
||||
isValidDate(day) &&
|
||||
!(disableWeekends && isWeekend(day)) &&
|
||||
!(disabled && isMatch(day, disabled))
|
||||
!(disabled && dateMatchModifiers(day, disabled))
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelect = (range) => {
|
||||
if (range?.from && range?.to) {
|
||||
setOpen(false);
|
||||
anchorRef?.focus();
|
||||
if (!isSameDay(range.from, range.to)) {
|
||||
setOpen(false);
|
||||
anchorRef?.focus();
|
||||
}
|
||||
}
|
||||
const prevToRange =
|
||||
!selectedRange?.from && selectedRange?.to ? selectedRange?.to : range?.to;
|
||||
@@ -415,7 +415,7 @@ export const useRangeDatepicker = (
|
||||
isValidDate: false,
|
||||
isInvalid: !isValidDate(day),
|
||||
isWeekend: disableWeekends && isWeekend(day),
|
||||
isDisabled: disabled && isMatch(day, disabled),
|
||||
isDisabled: disabled && dateMatchModifiers(day, disabled),
|
||||
isBefore,
|
||||
isAfter,
|
||||
},
|
||||
@@ -481,7 +481,7 @@ export const useRangeDatepicker = (
|
||||
isValidDate: false,
|
||||
isInvalid: !isValidDate(day),
|
||||
isWeekend: disableWeekends && isWeekend(day),
|
||||
isDisabled: disabled && isMatch(day, disabled),
|
||||
isDisabled: disabled && dateMatchModifiers(day, disabled),
|
||||
isBefore,
|
||||
isAfter,
|
||||
});
|
||||
@@ -1,13 +1,12 @@
|
||||
"use client";
|
||||
export { default as DatePicker, type DatePickerProps } from "./DatePicker";
|
||||
export { useDatepicker, type DateValidationT } from "./hooks/useDatepicker";
|
||||
export {
|
||||
useDatepicker,
|
||||
useRangeDatepicker,
|
||||
type DateValidationT,
|
||||
type RangeValidationT,
|
||||
} from "../hooks";
|
||||
} from "./hooks/useRangeDatepicker";
|
||||
export {
|
||||
DatePickerStandalone,
|
||||
type DatePickerStandaloneProps,
|
||||
} from "./DatePickerStandalone";
|
||||
export { DatePickerInput, type DateInputProps } from "../parts/DateInput";
|
||||
} from "./parts/DatePicker.Standalone";
|
||||
export { DatePickerInput, type DateInputProps } from "../Date.Input";
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from "react";
|
||||
import { CaptionProps, useDayPicker, useNavigation } from "react-day-picker";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons";
|
||||
import { Button } from "../../../button";
|
||||
import { Label } from "../../../typography";
|
||||
import { useDateTranslationContext } from "../../context";
|
||||
import WeekRow from "./WeekRow";
|
||||
|
||||
/**
|
||||
* https://github.com/gpbl/react-day-picker/tree/main/src/components/Caption
|
||||
*/
|
||||
export const DatePickerCaption = ({ displayMonth, id }: CaptionProps) => {
|
||||
const { goToMonth, nextMonth, previousMonth } = useNavigation();
|
||||
const {
|
||||
formatters: { formatCaption },
|
||||
locale,
|
||||
} = useDayPicker();
|
||||
const translate = useDateTranslationContext().translate;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="navds-date__caption">
|
||||
<Button
|
||||
variant="tertiary-neutral"
|
||||
disabled={!previousMonth}
|
||||
onClick={() => previousMonth && goToMonth(previousMonth)}
|
||||
icon={<ArrowLeftIcon title={translate("goToPreviousMonth")} />}
|
||||
className="navds-date__caption-button"
|
||||
type="button"
|
||||
/>
|
||||
<Label
|
||||
as="span"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
id={id}
|
||||
className="navds-date__caption-label"
|
||||
>
|
||||
{formatCaption(displayMonth, { locale })}
|
||||
</Label>
|
||||
<Button
|
||||
icon={<ArrowRightIcon title={translate("goToNextMonth")} />}
|
||||
onClick={() => nextMonth && goToMonth(nextMonth)}
|
||||
disabled={!nextMonth}
|
||||
variant="tertiary-neutral"
|
||||
className="navds-date__caption-button"
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
<WeekRow displayMonth={displayMonth} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePickerCaption;
|
||||
@@ -0,0 +1,56 @@
|
||||
import cl from "clsx";
|
||||
import { Locale, format } from "date-fns";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { CalendarDay, Modifiers } from "react-day-picker";
|
||||
|
||||
const DatePickerDayButton = ({
|
||||
day,
|
||||
modifiers,
|
||||
locale,
|
||||
children,
|
||||
...rest
|
||||
}: {
|
||||
day: CalendarDay;
|
||||
modifiers: Modifiers;
|
||||
locale: Locale;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (modifiers.focused) {
|
||||
ref.current?.focus();
|
||||
}
|
||||
}, [modifiers.focused]);
|
||||
|
||||
if (modifiers.hidden) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
ref={ref}
|
||||
aria-hidden={day.outside}
|
||||
aria-pressed={!!modifiers.selected}
|
||||
aria-label={format(day.date, "cccc d", {
|
||||
locale,
|
||||
})}
|
||||
data-pressed={modifiers.selected}
|
||||
data-today={modifiers.today || undefined}
|
||||
className={cl(rest.className, {
|
||||
"rdp-day_disabled": modifiers.disabled,
|
||||
"rdp-day_selected": modifiers.selected,
|
||||
"rdp-day_range_start": modifiers.range_start,
|
||||
"rdp-day_range_middle": modifiers.range_middle,
|
||||
"rdp-day_range_end": modifiers.range_end,
|
||||
"rdp-day_today": modifiers.today,
|
||||
"rdp-day_outside": modifiers.outside,
|
||||
"rdp-day__weekend": modifiers.weekend,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export { DatePickerDayButton };
|
||||
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
Locale,
|
||||
format,
|
||||
getMonth,
|
||||
getYear,
|
||||
setMonth,
|
||||
setYear,
|
||||
startOfMonth,
|
||||
} from "date-fns";
|
||||
import React, { ChangeEvent, useCallback } from "react";
|
||||
import { CalendarMonth, useDayPicker } from "react-day-picker";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons";
|
||||
import { Button } from "../../../button";
|
||||
import { Select } from "../../../form/select";
|
||||
import { BodyShort } from "../../../typography";
|
||||
import { omit } from "../../../util";
|
||||
import { useDateTranslationContext } from "../../Date.locale";
|
||||
import {
|
||||
calendarRange,
|
||||
getMonthOptions,
|
||||
getYearOptions,
|
||||
} from "../../date-utils";
|
||||
import { MultipleMode } from "../DatePicker.types";
|
||||
import { DatePickerWeekRow } from "./DatePicker.WeekRow";
|
||||
|
||||
const DatePickerMonths = ({
|
||||
children,
|
||||
calendarMonth,
|
||||
locale,
|
||||
onWeekNumberClick,
|
||||
...rest
|
||||
}: {
|
||||
calendarMonth: CalendarMonth;
|
||||
displayIndex: number;
|
||||
locale: Locale;
|
||||
onWeekNumberClick: MultipleMode["onWeekNumberClick"];
|
||||
} & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
const { dayPickerProps, goToMonth, previousMonth, nextMonth } =
|
||||
useDayPicker();
|
||||
|
||||
const { captionLayout } = dayPickerProps;
|
||||
|
||||
const translate = useDateTranslationContext().translate;
|
||||
|
||||
const handleMonthChange = useCallback(
|
||||
(date: Date, e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedMonth = Number(e.target.value);
|
||||
const newMonth = setMonth(startOfMonth(date), selectedMonth);
|
||||
goToMonth(newMonth);
|
||||
},
|
||||
[goToMonth],
|
||||
);
|
||||
|
||||
const handleYearChange = useCallback(
|
||||
(date: Date, e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedYear = Number(e.target.value);
|
||||
const newMonth = setYear(startOfMonth(date), selectedYear);
|
||||
goToMonth(newMonth);
|
||||
},
|
||||
[goToMonth],
|
||||
);
|
||||
|
||||
const [navStart, navEnd] = calendarRange({
|
||||
captionLayout: captionLayout === "dropdown" ? "dropdown" : "label",
|
||||
startMonth: dayPickerProps.startMonth,
|
||||
endMonth: dayPickerProps.endMonth,
|
||||
today: dayPickerProps.today,
|
||||
});
|
||||
|
||||
const months = getMonthOptions(calendarMonth.date, navStart, navEnd, locale);
|
||||
const dropdownYears = getYearOptions(navStart, navEnd, locale);
|
||||
|
||||
return (
|
||||
<div {...omit(rest, ["displayIndex"])}>
|
||||
<div className="navds-date__caption">
|
||||
{captionLayout?.startsWith("dropdown") && (
|
||||
<span aria-live="polite" aria-atomic="true" className="navds-sr-only">
|
||||
{format(calendarMonth.date, "LLLL y", { locale })}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="tertiary-neutral"
|
||||
disabled={!previousMonth}
|
||||
onClick={() => previousMonth && goToMonth(previousMonth)}
|
||||
icon={<ArrowLeftIcon title={translate("goToPreviousMonth")} />}
|
||||
className="navds-date__caption-button"
|
||||
type="button"
|
||||
/>
|
||||
{captionLayout?.startsWith("dropdown") ? (
|
||||
<div className="navds-date__caption">
|
||||
<Select
|
||||
label={translate("month")}
|
||||
hideLabel
|
||||
className="navds-date__caption__month"
|
||||
onChange={(event) => handleMonthChange(calendarMonth.date, event)}
|
||||
value={getMonth(calendarMonth.date)}
|
||||
>
|
||||
{months?.map(({ value, label, disabled }) => (
|
||||
<option key={value} value={value} disabled={disabled}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
label={translate("year")}
|
||||
hideLabel
|
||||
className="navds-date__caption__year"
|
||||
onChange={(event) => handleYearChange(calendarMonth.date, event)}
|
||||
value={getYear(calendarMonth.date)}
|
||||
>
|
||||
{dropdownYears?.map(({ value, label, disabled }) => (
|
||||
<option key={value} value={value} disabled={disabled}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
) : (
|
||||
<BodyShort
|
||||
weight="semibold"
|
||||
as="span"
|
||||
aria-live="polite"
|
||||
role="status"
|
||||
className="navds-date__caption-label"
|
||||
>
|
||||
{format(calendarMonth.date, "LLLL y", { locale })}
|
||||
</BodyShort>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="tertiary-neutral"
|
||||
icon={<ArrowRightIcon title={translate("goToNextMonth")} />}
|
||||
onClick={() => nextMonth && goToMonth(nextMonth)}
|
||||
disabled={!nextMonth}
|
||||
className="navds-date__caption-button"
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
<DatePickerWeekRow
|
||||
weeks={calendarMonth.weeks}
|
||||
onWeekNumberClick={onWeekNumberClick}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { DatePickerMonths };
|
||||
@@ -0,0 +1,170 @@
|
||||
import cl from "clsx";
|
||||
import { isWeekend } from "date-fns";
|
||||
import React, { useCallback } from "react";
|
||||
import { ClassNames, DayPicker, dateMatchModifiers } from "react-day-picker";
|
||||
import { Show } from "../../../layout/responsive";
|
||||
import { omit } from "../../../util";
|
||||
import { useDateLocale } from "../../../util/i18n/i18n.hooks";
|
||||
import { getLocaleFromString } from "../../Date.locale";
|
||||
import { clampDisplayMonth, isDateOutsideRange } from "../../date-utils";
|
||||
import {
|
||||
ConditionalModeProps,
|
||||
DatePickerDefaultProps,
|
||||
} from "../DatePicker.types";
|
||||
import { DatePickerDayButton } from "./DatePicker.DayButton";
|
||||
import { DatePickerMonths } from "./DatePicker.Months";
|
||||
import { DatePickerWeekNumber } from "./DatePicker.WeekNumber";
|
||||
|
||||
/**
|
||||
* To support backwards compatibility with the old datepicker,
|
||||
* we need to provide a partial implementation of the classnames
|
||||
*/
|
||||
const LegacyClassNames: Partial<ClassNames> = {
|
||||
root: "rdp",
|
||||
button_next: "button",
|
||||
day: "rdp-cell",
|
||||
day_button: "rdp-day rdp-button",
|
||||
/* We set this directly on DayButton */
|
||||
disabled: "",
|
||||
hidden: "rdp-day_hidden",
|
||||
outside: "rdp-day_outside",
|
||||
selected: "rdp-day_selected",
|
||||
weekday: "rdp-head_cell",
|
||||
weekdays: "rdp-head_row",
|
||||
week: "rdp-row",
|
||||
weeks: "rdp-tbody",
|
||||
month_grid: "rdp-table",
|
||||
week_number: "rdp-weeknumber",
|
||||
};
|
||||
|
||||
type ReactDayPickerProps = DatePickerDefaultProps &
|
||||
ConditionalModeProps & {
|
||||
/**
|
||||
* If datepicker should be fixed to 6 weeks, regardless of actual weeks in month
|
||||
* @default false
|
||||
*/
|
||||
fixedWeeks?: boolean;
|
||||
/**
|
||||
* Update selected date
|
||||
*/
|
||||
handleSelect: (newSelected: any) => void;
|
||||
};
|
||||
|
||||
const ReactDayPicker = ({
|
||||
className,
|
||||
dropdownCaption,
|
||||
disabled = [],
|
||||
disableWeekends = false,
|
||||
showWeekNumber = false,
|
||||
selected,
|
||||
fixedWeeks = false,
|
||||
onWeekNumberClick,
|
||||
fromDate,
|
||||
toDate,
|
||||
month,
|
||||
mode: _mode,
|
||||
handleSelect,
|
||||
locale: _locale,
|
||||
...rest
|
||||
}: ReactDayPickerProps) => {
|
||||
const langProviderLocale = useDateLocale();
|
||||
const locale = _locale ? getLocaleFromString(_locale) : langProviderLocale;
|
||||
|
||||
const mode = _mode ?? ("single" as any);
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
captionLayout={dropdownCaption ? "dropdown" : "label"}
|
||||
hideNavigation
|
||||
locale={locale}
|
||||
mode={mode as any}
|
||||
onSelect={handleSelect}
|
||||
selected={selected}
|
||||
classNames={LegacyClassNames}
|
||||
components={{
|
||||
MonthCaption: () => <></>,
|
||||
DayButton: useCallback(
|
||||
(props) => <DatePickerDayButton {...props} locale={locale} />,
|
||||
[locale],
|
||||
),
|
||||
Month: useCallback(
|
||||
(props) => (
|
||||
<DatePickerMonths
|
||||
{...props}
|
||||
locale={locale}
|
||||
onWeekNumberClick={
|
||||
mode === "multiple" ? onWeekNumberClick : undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
[locale, mode, onWeekNumberClick],
|
||||
),
|
||||
Day: useCallback(
|
||||
(props) => (
|
||||
<td
|
||||
{...omit(props, ["day", "modifiers"])}
|
||||
className="rdp-cell"
|
||||
role={undefined}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
),
|
||||
WeekNumber: useCallback(
|
||||
(props) => (
|
||||
<DatePickerWeekNumber
|
||||
{...props}
|
||||
showOnDesktop
|
||||
onWeekNumberClick={
|
||||
mode === "multiple" ? onWeekNumberClick : undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
[mode, onWeekNumberClick],
|
||||
),
|
||||
/* On smaller screens we hide it to accomedate our custom week-selector */
|
||||
WeekNumberHeader: useCallback(
|
||||
(props) => (
|
||||
<Show above="sm" asChild>
|
||||
<th {...props} />
|
||||
</Show>
|
||||
),
|
||||
[],
|
||||
),
|
||||
Weekdays: useCallback(
|
||||
(props) => (
|
||||
<thead {...props} className="rdp-head" aria-hidden>
|
||||
<tr className="rdp-head_row">{props.children}</tr>
|
||||
</thead>
|
||||
),
|
||||
[],
|
||||
),
|
||||
}}
|
||||
className={cl("navds-date", className)}
|
||||
disabled={(day) => {
|
||||
return (
|
||||
(disableWeekends && isWeekend(day)) ||
|
||||
dateMatchModifiers(day, disabled) ||
|
||||
isDateOutsideRange({ day, fromDate, toDate })
|
||||
);
|
||||
}}
|
||||
weekStartsOn={1}
|
||||
modifiers={{
|
||||
weekend: (day) => disableWeekends && isWeekend(day),
|
||||
}}
|
||||
modifiersClassNames={{
|
||||
weekend: "rdp-day__weekend",
|
||||
}}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={false}
|
||||
showWeekNumber={showWeekNumber}
|
||||
fixedWeeks={fixedWeeks}
|
||||
showOutsideDays
|
||||
startMonth={fromDate}
|
||||
endMonth={toDate}
|
||||
month={clampDisplayMonth({ month, start: fromDate, end: toDate })}
|
||||
{...omit(rest, ["onSelect", "role", "id", "defaultSelected"])}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { ReactDayPicker };
|
||||
@@ -0,0 +1,93 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import { DateRange } from "react-day-picker";
|
||||
import { useControllableState } from "../../../util/hooks";
|
||||
import { useI18n } from "../../../util/i18n/i18n.hooks";
|
||||
import {
|
||||
DateTranslationContextProvider,
|
||||
getTranslations,
|
||||
} from "../../Date.locale";
|
||||
import {
|
||||
DatePickerDefaultProps,
|
||||
MultipleMode,
|
||||
RangeMode,
|
||||
SingleMode,
|
||||
} from "../DatePicker.types";
|
||||
import { ReactDayPicker } from "./DatePicker.RDP";
|
||||
|
||||
interface DatePickerStandaloneDefaultProps
|
||||
extends Omit<
|
||||
DatePickerDefaultProps,
|
||||
"open" | "onClose" | "onOpenToggle" | "wrapperClassName" | "strategy"
|
||||
> {
|
||||
/**
|
||||
* Datepicker classname
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* If datepicker should be fixed to 6 weeks, regardless of actual weeks in month
|
||||
* @default true
|
||||
*/
|
||||
fixedWeeks?: boolean;
|
||||
}
|
||||
|
||||
type StandaloneConditionalModeProps = SingleMode | MultipleMode | RangeMode;
|
||||
|
||||
export type DatePickerStandaloneProps = DatePickerStandaloneDefaultProps &
|
||||
StandaloneConditionalModeProps;
|
||||
|
||||
export type DatePickerStandaloneType = React.ForwardRefExoticComponent<
|
||||
DatePickerStandaloneProps & React.RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
export const DatePickerStandalone: DatePickerStandaloneType = forwardRef<
|
||||
HTMLDivElement,
|
||||
DatePickerStandaloneProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
locale,
|
||||
translations,
|
||||
selected,
|
||||
defaultSelected,
|
||||
onSelect,
|
||||
mode,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const translate = useI18n(
|
||||
"DatePicker",
|
||||
translations,
|
||||
getTranslations(locale),
|
||||
);
|
||||
|
||||
const [value, setValue] = useControllableState<
|
||||
Date | Date[] | DateRange | undefined
|
||||
>({
|
||||
defaultValue: defaultSelected,
|
||||
value: selected,
|
||||
onChange: (newValue) => onSelect?.(newValue as any),
|
||||
});
|
||||
|
||||
return (
|
||||
<DateTranslationContextProvider translate={translate}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cl("navds-date__standalone-wrapper", className)}
|
||||
>
|
||||
<ReactDayPicker
|
||||
{...rest}
|
||||
locale={locale}
|
||||
handleSelect={setValue}
|
||||
selected={value as any}
|
||||
mode={mode as any}
|
||||
/>
|
||||
</div>
|
||||
</DateTranslationContextProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default DatePickerStandalone;
|
||||
@@ -0,0 +1,86 @@
|
||||
import cl from "clsx";
|
||||
import React, { useMemo } from "react";
|
||||
import { CalendarWeek, useDayPicker } from "react-day-picker";
|
||||
import { Button } from "../../../button";
|
||||
import { Hide, Show } from "../../../layout/responsive";
|
||||
import { Detail } from "../../../typography";
|
||||
import { useDateTranslationContext } from "../../Date.locale";
|
||||
import { MultipleMode } from "../DatePicker.types";
|
||||
|
||||
const DatePickerWeekNumber = ({
|
||||
week: { weekNumber, days },
|
||||
onWeekNumberClick,
|
||||
className,
|
||||
style,
|
||||
showOnDesktop,
|
||||
}: {
|
||||
week: CalendarWeek;
|
||||
onWeekNumberClick: MultipleMode["onWeekNumberClick"];
|
||||
showOnDesktop: boolean;
|
||||
} & React.ThHTMLAttributes<HTMLTableCellElement>) => {
|
||||
const translate = useDateTranslationContext().translate;
|
||||
|
||||
const { getModifiers } = useDayPicker();
|
||||
|
||||
const hideWeek = useMemo(() => {
|
||||
if (
|
||||
days.filter((day) => {
|
||||
const mods = getModifiers(day);
|
||||
return !(mods.hidden || mods.outside || mods.disabled);
|
||||
}).length === 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [days, getModifiers]);
|
||||
|
||||
const DisplayMode = showOnDesktop ? Show : Hide;
|
||||
|
||||
if (!onWeekNumberClick || hideWeek) {
|
||||
return (
|
||||
<DisplayMode above="sm" asChild>
|
||||
<td className="rdp-cell">
|
||||
<Detail
|
||||
as="span"
|
||||
textColor="subtle"
|
||||
className={className}
|
||||
style={style}
|
||||
aria-label={translate("weekNumber", { week: weekNumber })}
|
||||
>
|
||||
{weekNumber}
|
||||
</Detail>
|
||||
</td>
|
||||
</DisplayMode>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DisplayMode above="sm" asChild>
|
||||
<td
|
||||
className={cl("rdp-cell", {
|
||||
"navds-date__week-wrapper": !showOnDesktop,
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
variant="secondary-neutral"
|
||||
size="small"
|
||||
name="week-number"
|
||||
aria-label={translate("selectWeekNumber", { week: weekNumber })}
|
||||
style={style}
|
||||
className={cl("navds-date__weeknumber", "rdp-weeknumber")}
|
||||
onClick={() => {
|
||||
onWeekNumberClick(
|
||||
weekNumber,
|
||||
days.map((day) => day.date),
|
||||
);
|
||||
}}
|
||||
icon={
|
||||
<span className="navds-date__weeknumber-number">{weekNumber}</span>
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</DisplayMode>
|
||||
);
|
||||
};
|
||||
|
||||
export { DatePickerWeekNumber };
|
||||
+20
-21
@@ -1,14 +1,19 @@
|
||||
import React from "react";
|
||||
import { useDayPicker } from "react-day-picker";
|
||||
import { CalendarWeek } from "react-day-picker";
|
||||
import { Show } from "../../../layout/responsive";
|
||||
import { Detail } from "../../../typography";
|
||||
import { useId } from "../../../util/hooks";
|
||||
import { useDateTranslationContext } from "../../context";
|
||||
import { getMonthWeeks } from "../../utils/get-month-weeks";
|
||||
import WeekNumber from "./WeekNumber";
|
||||
import { useDateTranslationContext } from "../../Date.locale";
|
||||
import { MultipleMode } from "../DatePicker.types";
|
||||
import { DatePickerWeekNumber } from "./DatePicker.WeekNumber";
|
||||
|
||||
const WeekRow = ({ displayMonth }: { displayMonth: Date }) => {
|
||||
const { locale, fixedWeeks, onWeekNumberClick } = useDayPicker();
|
||||
const DatePickerWeekRow = ({
|
||||
onWeekNumberClick,
|
||||
weeks,
|
||||
}: {
|
||||
onWeekNumberClick: MultipleMode["onWeekNumberClick"];
|
||||
weeks?: CalendarWeek[];
|
||||
}) => {
|
||||
const translate = useDateTranslationContext().translate;
|
||||
const labelId = useId();
|
||||
|
||||
@@ -16,11 +21,6 @@ const WeekRow = ({ displayMonth }: { displayMonth: Date }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const weeks = getMonthWeeks(displayMonth, {
|
||||
useFixedWeeks: Boolean(fixedWeeks),
|
||||
locale,
|
||||
});
|
||||
|
||||
return (
|
||||
<Show below="sm" asChild>
|
||||
<table className="rdp-table" role="grid">
|
||||
@@ -32,19 +32,18 @@ const WeekRow = ({ displayMonth }: { displayMonth: Date }) => {
|
||||
className="rdp-cell navds-date__week-cell"
|
||||
>
|
||||
<span className="navds-date__week-wrapper" id={labelId}>
|
||||
{`${translate("week")}:`}
|
||||
{translate("week")}
|
||||
</span>
|
||||
</Detail>
|
||||
|
||||
{weeks.map((week) => (
|
||||
<td
|
||||
{weeks?.map((week) => (
|
||||
<DatePickerWeekNumber
|
||||
key={week.weekNumber}
|
||||
className="rdp-cell navds-date__week-cell"
|
||||
>
|
||||
<span className="navds-date__week-wrapper">
|
||||
<WeekNumber number={week.weekNumber} dates={week.dates} />
|
||||
</span>
|
||||
</td>
|
||||
week={week}
|
||||
onWeekNumberClick={onWeekNumberClick}
|
||||
showOnDesktop={false}
|
||||
className="navds-date__week-wrapper"
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -53,4 +52,4 @@ const WeekRow = ({ displayMonth }: { displayMonth: Date }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default WeekRow;
|
||||
export { DatePickerWeekRow };
|
||||
@@ -1,32 +0,0 @@
|
||||
import { format } from "date-fns";
|
||||
import React, { useRef } from "react";
|
||||
import { Button, DayProps, useDayPicker, useDayRender } from "react-day-picker";
|
||||
|
||||
const DayButton = (props: DayProps) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const dayRender = useDayRender(props.date, props.displayMonth, buttonRef);
|
||||
const { locale } = useDayPicker();
|
||||
const dateTime = format(props.date, "cccc d", { locale });
|
||||
|
||||
if (dayRender.isHidden) {
|
||||
return <></>;
|
||||
}
|
||||
if (!dayRender.isButton) {
|
||||
return <div {...dayRender.divProps} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
name="day"
|
||||
ref={buttonRef}
|
||||
{...dayRender.buttonProps}
|
||||
role={undefined}
|
||||
aria-label={dateTime}
|
||||
aria-hidden={dayRender.activeModifiers.outside}
|
||||
aria-selected={undefined}
|
||||
aria-pressed={!!dayRender.activeModifiers.selected}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DayButton;
|
||||
@@ -1,110 +0,0 @@
|
||||
import { max, min, setMonth, setYear, startOfMonth } from "date-fns";
|
||||
import React from "react";
|
||||
import { CaptionProps, useDayPicker, useNavigation } from "react-day-picker";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons";
|
||||
import { Button } from "../../../button";
|
||||
import { Select } from "../../../form/select";
|
||||
import { useDateTranslationContext } from "../../context";
|
||||
import { getMonths, getYears } from "../../utils";
|
||||
import WeekRow from "./WeekRow";
|
||||
|
||||
/**
|
||||
* https://github.com/gpbl/react-day-picker/tree/main/src/components/CaptionDropdowns
|
||||
*/
|
||||
export const DropdownCaption = ({ displayMonth, id }: CaptionProps) => {
|
||||
const { goToMonth, nextMonth, previousMonth } = useNavigation();
|
||||
const {
|
||||
fromDate,
|
||||
toDate,
|
||||
formatters: { formatYearCaption, formatMonthCaption, formatCaption },
|
||||
locale,
|
||||
} = useDayPicker();
|
||||
const translate = useDateTranslationContext().translate;
|
||||
|
||||
if (!fromDate || !toDate) {
|
||||
console.warn("Using dropdownCaption required fromDate and toDate");
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleYearChange: React.ChangeEventHandler<HTMLSelectElement> = (e) => {
|
||||
const newMonth = setYear(
|
||||
startOfMonth(displayMonth),
|
||||
Number(e.target.value),
|
||||
);
|
||||
goToMonth(startOfMonth(min([max([newMonth, fromDate]), toDate])));
|
||||
};
|
||||
|
||||
const handleMonthChange: React.ChangeEventHandler<HTMLSelectElement> = (e) =>
|
||||
goToMonth(setMonth(startOfMonth(displayMonth), Number(e.target.value)));
|
||||
|
||||
const years = getYears(
|
||||
fromDate,
|
||||
toDate,
|
||||
displayMonth.getFullYear(),
|
||||
).reverse();
|
||||
const months = getMonths(fromDate, toDate, displayMonth);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="navds-date__caption">
|
||||
<span
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
id={id}
|
||||
className="navds-sr-only"
|
||||
>
|
||||
{formatCaption(displayMonth, { locale })}
|
||||
</span>
|
||||
<Button
|
||||
variant="tertiary-neutral"
|
||||
disabled={!previousMonth}
|
||||
onClick={() => previousMonth && goToMonth(previousMonth)}
|
||||
icon={<ArrowLeftIcon title={translate("goToPreviousMonth")} />}
|
||||
className="navds-date__caption-button"
|
||||
type="button"
|
||||
/>
|
||||
|
||||
<div className="navds-date__caption">
|
||||
<Select
|
||||
label={translate("month")}
|
||||
hideLabel
|
||||
className="navds-date__caption__month"
|
||||
value={displayMonth.getMonth()}
|
||||
onChange={handleMonthChange}
|
||||
>
|
||||
{months.map((m) => (
|
||||
<option key={m.getMonth()} value={m.getMonth()}>
|
||||
{formatMonthCaption(m, { locale })}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
label={translate("year")}
|
||||
hideLabel
|
||||
value={displayMonth.getFullYear()}
|
||||
onChange={handleYearChange}
|
||||
className="navds-date__caption__year"
|
||||
>
|
||||
{years.map((year) => (
|
||||
<option key={year.getFullYear()} value={year.getFullYear()}>
|
||||
{formatYearCaption(year, { locale })}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="tertiary-neutral"
|
||||
icon={<ArrowRightIcon title={translate("goToNextMonth")} />}
|
||||
onClick={() => nextMonth && goToMonth(nextMonth)}
|
||||
disabled={!nextMonth}
|
||||
className="navds-date__caption-button"
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
<WeekRow displayMonth={displayMonth} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownCaption;
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Locale, addDays, startOfWeek } from "date-fns";
|
||||
import React from "react";
|
||||
import { useDayPicker } from "react-day-picker";
|
||||
import { Hide } from "../../../layout/responsive";
|
||||
|
||||
/**
|
||||
* https://github.com/gpbl/react-day-picker/tree/main/src/components/HeadRow
|
||||
*/
|
||||
export function HeadRow(): JSX.Element {
|
||||
const {
|
||||
classNames,
|
||||
styles,
|
||||
showWeekNumber,
|
||||
locale,
|
||||
formatters: { formatWeekdayName },
|
||||
labels: { labelWeekday },
|
||||
} = useDayPicker();
|
||||
|
||||
const weekdays = getWeekdays(locale);
|
||||
|
||||
return (
|
||||
<tr style={styles.head_row} className={classNames.head_row}>
|
||||
{showWeekNumber && (
|
||||
<Hide below="sm" asChild>
|
||||
<td style={styles.head_cell} className={classNames.head_cell}></td>
|
||||
</Hide>
|
||||
)}
|
||||
{weekdays.map((weekday, i) => (
|
||||
<th
|
||||
key={i}
|
||||
scope="col"
|
||||
className={classNames.head_cell}
|
||||
style={styles.head_cell}
|
||||
aria-label={labelWeekday(weekday, { locale })}
|
||||
>
|
||||
{formatWeekdayName(weekday, { locale })}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a series of 7 days, starting from the week, to use for formatting
|
||||
* the weekday names (Monday, Tuesday, etc.).
|
||||
*/
|
||||
export function getWeekdays(locale?: Locale): Date[] {
|
||||
const start = startOfWeek(new Date(), { locale, weekStartsOn: 1 });
|
||||
|
||||
const days: Date[] = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const day = addDays(start, i);
|
||||
days.push(day);
|
||||
}
|
||||
return days;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { getUnixTime } from "date-fns";
|
||||
import React from "react";
|
||||
import { useDayPicker } from "react-day-picker";
|
||||
import { Hide } from "../../../layout/responsive";
|
||||
import DayButton from "./DayButton";
|
||||
import WeekNumber from "./WeekNumber";
|
||||
|
||||
/**
|
||||
* The props for the {@link Row} component.
|
||||
*/
|
||||
export interface RowProps {
|
||||
/** The month where the row is displayed. */
|
||||
displayMonth: Date;
|
||||
/** The number of the week to render. */
|
||||
weekNumber: number;
|
||||
/** The days contained in the week. */
|
||||
dates: Date[];
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/gpbl/react-day-picker/tree/main/src/components/Row
|
||||
*/
|
||||
function Row(props: RowProps): JSX.Element {
|
||||
const { styles, classNames, showWeekNumber } = useDayPicker();
|
||||
|
||||
return (
|
||||
<tr className={classNames.row} style={styles.row}>
|
||||
{showWeekNumber && (
|
||||
<Hide below="sm" asChild>
|
||||
<td className={classNames.cell} style={styles.cell}>
|
||||
<WeekNumber number={props.weekNumber} dates={props.dates} />
|
||||
</td>
|
||||
</Hide>
|
||||
)}
|
||||
{props.dates.map((date) => (
|
||||
<td
|
||||
className={classNames.cell}
|
||||
style={styles.cell}
|
||||
key={getUnixTime(date)}
|
||||
>
|
||||
<DayButton displayMonth={props.displayMonth} date={date} />
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default Row;
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from "react";
|
||||
import { HeadRow, useDayPicker } from "react-day-picker";
|
||||
|
||||
/**
|
||||
* https://github.com/gpbl/react-day-picker/tree/main/src/components/Head
|
||||
*/
|
||||
function TableHead(): JSX.Element {
|
||||
const { classNames, styles, components } = useDayPicker();
|
||||
const HeadRowComponent = components?.HeadRow ?? HeadRow;
|
||||
return (
|
||||
<thead style={styles.head} className={classNames.head} aria-hidden>
|
||||
<HeadRowComponent />
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
|
||||
export default TableHead;
|
||||
@@ -1,75 +0,0 @@
|
||||
/* https://github.com/gpbl/react-day-picker/blob/7f78cd5/src/components/WeekNumber/WeekNumber.tsx#L21 */
|
||||
import React from "react";
|
||||
import { Button as RDPButton, useDayPicker } from "react-day-picker";
|
||||
import { Button } from "../../../button";
|
||||
import { useThemeInternal } from "../../../theme/Theme";
|
||||
import { Detail } from "../../../typography";
|
||||
import { useDateTranslationContext } from "../../context";
|
||||
|
||||
export interface WeekNumberProps {
|
||||
/** The number of the week. */
|
||||
number: number;
|
||||
/** The dates in the week. */
|
||||
dates: Date[];
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/gpbl/react-day-picker/tree/main/src/components/WeekNumber
|
||||
*/
|
||||
function WeekNumber({
|
||||
number: weekNumber,
|
||||
dates,
|
||||
}: WeekNumberProps): JSX.Element {
|
||||
const { onWeekNumberClick, styles, classNames } = useDayPicker();
|
||||
const themeContext = useThemeInternal(false);
|
||||
const translate = useDateTranslationContext().translate;
|
||||
|
||||
if (!onWeekNumberClick) {
|
||||
return (
|
||||
<Detail
|
||||
as="span"
|
||||
textColor="subtle"
|
||||
className={classNames.weeknumber}
|
||||
style={styles.weeknumber}
|
||||
aria-label={translate("weekNumber", { week: weekNumber })}
|
||||
>
|
||||
{weekNumber}
|
||||
</Detail>
|
||||
);
|
||||
}
|
||||
|
||||
if (themeContext) {
|
||||
return (
|
||||
<Button
|
||||
variant="secondary-neutral"
|
||||
size="small"
|
||||
name="week-number"
|
||||
aria-label={translate("selectWeekNumber", { week: weekNumber })}
|
||||
style={styles.weeknumber}
|
||||
className="navds-date__weeknumber"
|
||||
onClick={(event) => {
|
||||
onWeekNumberClick(weekNumber, dates, event);
|
||||
}}
|
||||
icon={
|
||||
<span className="navds-date__weeknumber-number">{weekNumber}</span>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RDPButton
|
||||
name="week-number"
|
||||
aria-label={translate("selectWeekNumber", { week: weekNumber })}
|
||||
style={styles.weeknumber}
|
||||
className={classNames.weeknumber}
|
||||
onClick={(event) => {
|
||||
onWeekNumberClick(weekNumber, dates, event);
|
||||
}}
|
||||
>
|
||||
{weekNumber}
|
||||
</RDPButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default WeekNumber;
|
||||
@@ -1,6 +0,0 @@
|
||||
export { useDatepicker } from "./useDatepicker";
|
||||
export type { DateValidationT } from "./useDatepicker";
|
||||
export { useRangeDatepicker } from "./useRangeDatepicker";
|
||||
export type { RangeValidationT } from "./useRangeDatepicker";
|
||||
export { useMonthpicker } from "./useMonthPicker";
|
||||
export type { MonthValidationT } from "./useMonthPicker";
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Locale, isSameYear } from "date-fns";
|
||||
import React from "react";
|
||||
import { createContext } from "../../util/create-context";
|
||||
import { useControllableState } from "../../util/hooks";
|
||||
import { clampDisplayYear } from "../date-utils";
|
||||
import { MonthPickerProps } from "./MonthPicker.types";
|
||||
|
||||
type MonthPickerProviderProps = Pick<
|
||||
MonthPickerProps,
|
||||
| "dropdownCaption"
|
||||
| "disabled"
|
||||
| "selected"
|
||||
| "defaultSelected"
|
||||
| "onMonthSelect"
|
||||
| "year"
|
||||
| "onYearChange"
|
||||
| "fromDate"
|
||||
| "toDate"
|
||||
> & {
|
||||
children: React.ReactNode;
|
||||
locale: Locale;
|
||||
};
|
||||
|
||||
type MonthPickerContextProps = {
|
||||
caption: "label" | "dropdown";
|
||||
year: Date;
|
||||
onYearChange: (year: Date) => void;
|
||||
selected: MonthPickerProps["selected"];
|
||||
onMonthSelect: MonthPickerProps["onMonthSelect"];
|
||||
disabled: NonNullable<MonthPickerProps["disabled"]>;
|
||||
locale: Locale;
|
||||
fromDate?: MonthPickerProps["fromDate"];
|
||||
toDate?: MonthPickerProps["toDate"];
|
||||
};
|
||||
|
||||
const [MonthPickerContextProvider, useMonthPickerContext] =
|
||||
createContext<MonthPickerContextProps>({
|
||||
name: "MonthPickerContext",
|
||||
errorMessage:
|
||||
"useMonthPickerContext must be used within an MonthPickerContextProvider",
|
||||
});
|
||||
|
||||
const MonthPickerProvider = ({
|
||||
children,
|
||||
disabled = [],
|
||||
year,
|
||||
onYearChange,
|
||||
fromDate,
|
||||
toDate,
|
||||
selected,
|
||||
defaultSelected,
|
||||
onMonthSelect,
|
||||
dropdownCaption,
|
||||
locale,
|
||||
}: MonthPickerProviderProps) => {
|
||||
/**
|
||||
* Controlls current displayed year in MonthPicker
|
||||
*/
|
||||
const [displayYear, setDisplayYear] = useControllableState({
|
||||
defaultValue:
|
||||
clampDisplayYear({
|
||||
month: year ?? selected ?? defaultSelected,
|
||||
start: fromDate,
|
||||
end: toDate,
|
||||
}) ?? new Date(),
|
||||
value: year,
|
||||
onChange: onYearChange,
|
||||
});
|
||||
|
||||
/**
|
||||
* Allows both controlled and uncontrolled use of MonthPicker
|
||||
*/
|
||||
const [month, setMonth] = useControllableState({
|
||||
defaultValue: defaultSelected,
|
||||
value: selected,
|
||||
onChange: onMonthSelect,
|
||||
});
|
||||
|
||||
const handleDisplayYearUpdate = (newYear: Date) => {
|
||||
if (isSameYear(newYear, displayYear)) {
|
||||
return;
|
||||
}
|
||||
setDisplayYear(newYear);
|
||||
};
|
||||
|
||||
return (
|
||||
<MonthPickerContextProvider
|
||||
caption={dropdownCaption && fromDate && toDate ? "dropdown" : "label"}
|
||||
disabled={disabled}
|
||||
selected={month}
|
||||
onMonthSelect={setMonth}
|
||||
year={displayYear}
|
||||
onYearChange={handleDisplayYearUpdate}
|
||||
locale={locale}
|
||||
fromDate={fromDate}
|
||||
toDate={toDate}
|
||||
>
|
||||
{children}
|
||||
</MonthPickerContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export { MonthPickerProvider, useMonthPickerContext };
|
||||
@@ -0,0 +1,307 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import { expect, fn, userEvent, within } from "@storybook/test";
|
||||
import { format, setYear } from "date-fns";
|
||||
import { nb } from "date-fns/locale";
|
||||
import React from "react";
|
||||
import MonthPicker from "./MonthPicker";
|
||||
|
||||
export default {
|
||||
title: "ds-react/Monthpicker/Tests",
|
||||
component: MonthPicker,
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
} satisfies Meta<typeof MonthPicker>;
|
||||
|
||||
type Story = StoryObj<typeof MonthPicker>;
|
||||
|
||||
const currentDate = new Date();
|
||||
const previousYear = setYear(currentDate, currentDate.getFullYear() - 1);
|
||||
const nextYear = setYear(currentDate, currentDate.getFullYear() + 1);
|
||||
|
||||
/**
|
||||
* Validate that the monthpicker shows the current year by default
|
||||
*/
|
||||
export const InitialDefaultView: Story = {
|
||||
render: () => <MonthPicker.Standalone />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const label = canvas.getByText(`${new Date().getFullYear()}`);
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
|
||||
const todayButton = canvas.getByRole("button", {
|
||||
name: format(new Date(), "LLLL", { locale: nb }),
|
||||
});
|
||||
|
||||
expect(todayButton.getAttribute("data-current-month")).toBe("true");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that the monthpicker shows the previous year when `year`-prop is set to the previous year
|
||||
*/
|
||||
export const PreviousYearView: Story = {
|
||||
render: () => <MonthPicker.Standalone year={previousYear} />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const label = canvas.getByText(`${previousYear.getFullYear()}`);
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
|
||||
const todayButton = canvas.getByRole("button", {
|
||||
name: format(previousYear, "LLLL", { locale: nb }),
|
||||
});
|
||||
|
||||
expect(todayButton.getAttribute("data-current-month")).toBe("false");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that the monthpicker shows the next year when `year`-prop is set to the next year
|
||||
*/
|
||||
export const NextYearView: Story = {
|
||||
render: () => <MonthPicker.Standalone year={nextYear} />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const label = canvas.getByText(`${nextYear.getFullYear()}`);
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
|
||||
const todayButton = canvas.getByRole("button", {
|
||||
name: format(nextYear, "LLLL", { locale: nb }),
|
||||
});
|
||||
|
||||
expect(todayButton.getAttribute("data-current-month")).toBe("false");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that the monthpicker shows the previous year when initial `selected` is set to the previous year
|
||||
*/
|
||||
export const InitialControlledView: Story = {
|
||||
render: () => <MonthPicker.Standalone selected={previousYear} />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const label = canvas.getByText(`${previousYear.getFullYear()}`);
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
|
||||
const todayButton = canvas.getByRole("button", {
|
||||
name: format(previousYear, "LLLL", { locale: nb }),
|
||||
});
|
||||
|
||||
expect(todayButton.getAttribute("data-current-month")).toBe("false");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that the monthpicker shows the previous year when initial `defaultSelected` is set to the previous year
|
||||
*/
|
||||
export const InitialDefaultControlledView: Story = {
|
||||
render: () => <MonthPicker.Standalone defaultSelected={previousYear} />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const label = canvas.getByText(`${previousYear.getFullYear()}`);
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
|
||||
const todayButton = canvas.getByRole("button", {
|
||||
name: format(previousYear, "LLLL", { locale: nb }),
|
||||
});
|
||||
|
||||
expect(todayButton.getAttribute("data-current-month")).toBe("false");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that the monthpicker handles `disabled` prop correctly
|
||||
*/
|
||||
export const DisabledMonths: Story = {
|
||||
render: () => (
|
||||
<MonthPicker.Standalone
|
||||
year={new Date("2022")}
|
||||
disabled={[
|
||||
{ from: new Date("Feb 1 2022"), to: new Date("May 6 2022") },
|
||||
new Date("Sep 5 2022"),
|
||||
]}
|
||||
/>
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const disabledButtons = [
|
||||
canvas.getByRole("button", {
|
||||
name: format(new Date("Feb 1 2023"), "LLLL", { locale: nb }),
|
||||
}),
|
||||
canvas.getByRole("button", {
|
||||
name: format(new Date("Mar 1 2023"), "LLLL", { locale: nb }),
|
||||
}),
|
||||
canvas.getByRole("button", {
|
||||
name: format(new Date("Apr 1 2023"), "LLLL", { locale: nb }),
|
||||
}),
|
||||
canvas.getByRole("button", {
|
||||
name: format(new Date("May 1 2023"), "LLLL", { locale: nb }),
|
||||
}),
|
||||
canvas.getByRole("button", {
|
||||
name: format(new Date("Sep 1 2023"), "LLLL", { locale: nb }),
|
||||
}),
|
||||
];
|
||||
|
||||
disabledButtons.forEach((button) => {
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that the monthpicker handles `dropdownCaption` prop correctly
|
||||
*/
|
||||
export const LabelCaptionView: Story = {
|
||||
render: () => {
|
||||
const [year, setLocalYear] = React.useState(new Date("2025"));
|
||||
|
||||
return (
|
||||
<MonthPicker.Standalone
|
||||
fromDate={new Date("Feb 10 2022")}
|
||||
toDate={new Date("Sep 27 2028")}
|
||||
year={year}
|
||||
onYearChange={(y) => y && setLocalYear(y)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const label = canvas.getByText("2025");
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
|
||||
await userEvent.keyboard("{Tab}");
|
||||
await userEvent.keyboard("{Enter}");
|
||||
|
||||
expect(label.textContent).toBe("2024");
|
||||
|
||||
await userEvent.keyboard("{Tab}");
|
||||
await userEvent.keyboard("{Enter}");
|
||||
await userEvent.keyboard("{Enter}");
|
||||
expect(label.textContent).toBe("2026");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that the monthpicker handles `dropdownCaption` prop correctly
|
||||
*/
|
||||
export const DropdownCaptionView: Story = {
|
||||
render: () => {
|
||||
const [year, setLocalYear] = React.useState(new Date("2025"));
|
||||
|
||||
return (
|
||||
<MonthPicker.Standalone
|
||||
dropdownCaption
|
||||
fromDate={new Date("Feb 10 2022")}
|
||||
toDate={new Date("Sep 27 2028")}
|
||||
year={year}
|
||||
onYearChange={(y) => y && setLocalYear(y)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const dropdown = canvas.getByDisplayValue("2025");
|
||||
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
|
||||
await userEvent.selectOptions(dropdown, "2023");
|
||||
expect((dropdown as HTMLSelectElement).value).toBe("2023");
|
||||
|
||||
await userEvent.keyboard("{Tab}");
|
||||
await userEvent.keyboard("{Enter}");
|
||||
|
||||
expect((dropdown as HTMLSelectElement).value).toBe("2024");
|
||||
|
||||
await userEvent.selectOptions(dropdown, "2028");
|
||||
|
||||
expect((dropdown as HTMLSelectElement).value).toBe("2028");
|
||||
|
||||
await userEvent.keyboard("{Tab}");
|
||||
await userEvent.keyboard("{Enter}");
|
||||
|
||||
expect((dropdown as HTMLSelectElement).value).toBe("2028");
|
||||
|
||||
await userEvent.selectOptions(dropdown, "2022");
|
||||
expect((dropdown as HTMLSelectElement).value).toBe("2022");
|
||||
|
||||
await userEvent.tab({ shift: true });
|
||||
await userEvent.keyboard("{Enter}");
|
||||
expect((dropdown as HTMLSelectElement).value).toBe("2022");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that the monthpicker handles `dropdownCaption` prop correctly
|
||||
*/
|
||||
export const MonthSelect: StoryObj<{
|
||||
onMonthSelect: ReturnType<typeof fn>;
|
||||
}> = {
|
||||
args: {
|
||||
onMonthSelect: fn(),
|
||||
},
|
||||
render: (props) => {
|
||||
const [year, setLocalYear] = React.useState(new Date("2025"));
|
||||
|
||||
return (
|
||||
<MonthPicker.Standalone
|
||||
dropdownCaption
|
||||
fromDate={new Date("Feb 10 2022")}
|
||||
toDate={new Date("Sep 27 2028")}
|
||||
year={year}
|
||||
onYearChange={(y) => y && setLocalYear(y)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
args.onMonthSelect.mockClear();
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const dropdown = canvas.getByDisplayValue("2025");
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
|
||||
let febButton = canvas.getByRole("button", {
|
||||
name: format(new Date("Feb 01 2025"), "LLLL", { locale: nb }),
|
||||
});
|
||||
expect(febButton).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(febButton);
|
||||
|
||||
expect(args.onMonthSelect).toBeCalledTimes(1);
|
||||
expect(args.onMonthSelect.mock.calls[0][0].toString()).toEqual(
|
||||
new Date("Feb 01 2025").toString(),
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(dropdown, "2023");
|
||||
|
||||
febButton = canvas.getByRole("button", {
|
||||
name: format(new Date("Feb 01 2023"), "LLLL", { locale: nb }),
|
||||
});
|
||||
expect(febButton).toBeInTheDocument();
|
||||
await userEvent.click(febButton);
|
||||
|
||||
expect(args.onMonthSelect).toBeCalledTimes(2);
|
||||
expect(args.onMonthSelect.mock.calls[1][0].toString()).toEqual(
|
||||
new Date("Feb 01 2023").toString(),
|
||||
);
|
||||
|
||||
await userEvent.click(febButton);
|
||||
|
||||
expect(args.onMonthSelect).toBeCalledTimes(3);
|
||||
expect(args.onMonthSelect.mock.calls[2][0]).toBeUndefined();
|
||||
},
|
||||
};
|
||||
@@ -1,21 +1,20 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef, useState } from "react";
|
||||
import { DayPickerProvider } from "react-day-picker";
|
||||
import { useId } from "../../util/hooks";
|
||||
import { useControllableState, useId } from "../../util/hooks";
|
||||
import { useMergeRefs } from "../../util/hooks/useMergeRefs";
|
||||
import { useDateLocale, useI18n } from "../../util/i18n/i18n.hooks";
|
||||
import { DateDialog } from "../Date.Dialog";
|
||||
import { DateInputContextProvider, MonthPickerInput } from "../Date.Input";
|
||||
import {
|
||||
DateInputContext,
|
||||
DateTranslationContextProvider,
|
||||
SharedMonthProvider,
|
||||
} from "../context";
|
||||
import { MonthPickerInput } from "../parts/DateInput";
|
||||
import { DateWrapper } from "../parts/DateWrapper";
|
||||
import { getLocaleFromString, getTranslations } from "../utils";
|
||||
import MonthCaption from "./MonthCaption";
|
||||
import MonthPickerStandalone from "./MonthPickerStandalone";
|
||||
import MonthSelector from "./MonthSelector";
|
||||
import { MonthPickerProps } from "./types";
|
||||
getLocaleFromString,
|
||||
getTranslations,
|
||||
} from "../Date.locale";
|
||||
import { MonthPickerProvider } from "./MonthPicker.context";
|
||||
import { MonthPickerProps } from "./MonthPicker.types";
|
||||
import { MonthPickerCaption } from "./parts/MonthPicker.Caption";
|
||||
import { MonthPickerStandalone } from "./parts/MonthPicker.Standalone";
|
||||
import { MonthPickerTable } from "./parts/MonthPicker.Table";
|
||||
|
||||
interface MonthPickerComponent
|
||||
extends React.ForwardRefExoticComponent<MonthPickerProps> {
|
||||
@@ -91,20 +90,26 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
|
||||
);
|
||||
const langProviderLocale = useDateLocale();
|
||||
const ariaId = useId(id);
|
||||
const [open, setOpen] = useState(_open ?? false);
|
||||
|
||||
const [open, setOpen] = useControllableState({
|
||||
defaultValue: false,
|
||||
value: _open,
|
||||
onChange: () => {
|
||||
onOpenToggle?.();
|
||||
},
|
||||
});
|
||||
|
||||
/* We use state here to insure that anchor is defined if open is true on initial render */
|
||||
const [wrapperRef, setWrapperRef] = useState<HTMLDivElement | null>(null);
|
||||
const mergedRef = useMergeRefs(setWrapperRef, ref);
|
||||
|
||||
const [selectedMonth, setSelectedMonth] = useState<Date | undefined>(
|
||||
defaultSelected,
|
||||
);
|
||||
|
||||
const handleSelect = (month?: Date) => {
|
||||
!onMonthSelect && setSelectedMonth(month);
|
||||
onMonthSelect?.(month);
|
||||
month && (onClose?.() ?? setOpen(false));
|
||||
|
||||
if (month) {
|
||||
onClose?.();
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (dropdownCaption && (!fromDate || !toDate)) {
|
||||
@@ -114,62 +119,52 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
|
||||
|
||||
return (
|
||||
<DateTranslationContextProvider translate={translate}>
|
||||
<DateInputContext.Provider
|
||||
value={{
|
||||
open: _open ?? open,
|
||||
onOpen: () => {
|
||||
setOpen((x) => !x);
|
||||
onOpenToggle?.();
|
||||
},
|
||||
ariaId,
|
||||
defined: true,
|
||||
}}
|
||||
<DateInputContextProvider
|
||||
open={open}
|
||||
onOpen={() => setOpen((x) => !x)}
|
||||
ariaId={ariaId}
|
||||
defined={true}
|
||||
>
|
||||
<div
|
||||
ref={mergedRef}
|
||||
className={cl("navds-date__wrapper", wrapperClassName)}
|
||||
<MonthPickerProvider
|
||||
dropdownCaption={dropdownCaption}
|
||||
defaultSelected={defaultSelected}
|
||||
selected={selected}
|
||||
disabled={disabled}
|
||||
fromDate={fromDate}
|
||||
toDate={toDate}
|
||||
year={year}
|
||||
onYearChange={onYearChange}
|
||||
onMonthSelect={handleSelect}
|
||||
locale={locale ? getLocaleFromString(locale) : langProviderLocale}
|
||||
>
|
||||
{children}
|
||||
<DateWrapper
|
||||
open={_open ?? open}
|
||||
anchor={wrapperRef}
|
||||
onClose={() => onClose?.() ?? setOpen(false)}
|
||||
locale={locale}
|
||||
translate={translate}
|
||||
variant="month"
|
||||
popoverProps={{
|
||||
id: ariaId,
|
||||
strategy,
|
||||
}}
|
||||
<div
|
||||
ref={mergedRef}
|
||||
className={cl("navds-date__wrapper", wrapperClassName)}
|
||||
>
|
||||
<DayPickerProvider
|
||||
initialProps={{
|
||||
locale: locale
|
||||
? getLocaleFromString(locale)
|
||||
: langProviderLocale,
|
||||
selected: selected ?? selectedMonth,
|
||||
toDate,
|
||||
fromDate,
|
||||
month: selected ?? selectedMonth,
|
||||
{children}
|
||||
<DateDialog
|
||||
open={open}
|
||||
anchor={wrapperRef}
|
||||
onClose={() => {
|
||||
onClose?.();
|
||||
open && setOpen(false);
|
||||
}}
|
||||
locale={locale}
|
||||
translate={translate}
|
||||
variant="month"
|
||||
popoverProps={{
|
||||
id: ariaId,
|
||||
strategy,
|
||||
}}
|
||||
>
|
||||
<div className={cl("rdp-month", className)}>
|
||||
<SharedMonthProvider
|
||||
dropdownCaption={dropdownCaption}
|
||||
disabled={disabled}
|
||||
selected={selected ?? selectedMonth}
|
||||
onSelect={handleSelect}
|
||||
year={year}
|
||||
onYearChange={onYearChange}
|
||||
>
|
||||
<MonthCaption />
|
||||
<MonthSelector />
|
||||
</SharedMonthProvider>
|
||||
<MonthPickerCaption />
|
||||
<MonthPickerTable />
|
||||
</div>
|
||||
</DayPickerProvider>
|
||||
</DateWrapper>
|
||||
</div>
|
||||
</DateInputContext.Provider>
|
||||
</DateDialog>
|
||||
</div>
|
||||
</MonthPickerProvider>
|
||||
</DateInputContextProvider>
|
||||
</DateTranslationContextProvider>
|
||||
);
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { ComponentTranslation } from "../../util/i18n/i18n.types";
|
||||
import { Matcher } from "../utils";
|
||||
import { Matcher } from "../Date.typeutils";
|
||||
|
||||
export interface MonthPickerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import { setYear } from "date-fns";
|
||||
import { Matcher, isMatch } from "./is-match";
|
||||
import { Matcher } from "../Date.typeutils";
|
||||
import { isMatch } from "../date-utils";
|
||||
|
||||
export const nextEnabled = (
|
||||
months: Date[],
|
||||
@@ -1,102 +0,0 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef, useState } from "react";
|
||||
import { DayPickerProvider } from "react-day-picker";
|
||||
import { useDateLocale, useI18n } from "../../util/i18n/i18n.hooks";
|
||||
import {
|
||||
DateTranslationContextProvider,
|
||||
SharedMonthProvider,
|
||||
} from "../context";
|
||||
import { getLocaleFromString, getTranslations } from "../utils";
|
||||
import MonthCaption from "./MonthCaption";
|
||||
import MonthSelector from "./MonthSelector";
|
||||
import { MonthPickerProps } from "./types";
|
||||
|
||||
export interface MonthPickerStandaloneProps
|
||||
extends Omit<
|
||||
MonthPickerProps,
|
||||
"open" | "onClose" | "onOpenToggle" | "wrapperClassName" | "strategy"
|
||||
> {
|
||||
/**
|
||||
* Monthpicker classname
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type MonthPickerStandaloneType = React.ForwardRefExoticComponent<
|
||||
MonthPickerStandaloneProps & React.RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
export const MonthPickerStandalone = forwardRef<
|
||||
HTMLDivElement,
|
||||
MonthPickerStandaloneProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
dropdownCaption = false,
|
||||
fromDate,
|
||||
toDate,
|
||||
disabled = [],
|
||||
selected,
|
||||
className,
|
||||
locale,
|
||||
translations,
|
||||
onMonthSelect,
|
||||
defaultSelected,
|
||||
year,
|
||||
onYearChange,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const translate = useI18n(
|
||||
"DatePicker",
|
||||
translations,
|
||||
getTranslations(locale),
|
||||
);
|
||||
const langProviderLocale = useDateLocale();
|
||||
const [selectedMonth, setSelectedMonth] = useState<Date | undefined>(
|
||||
defaultSelected,
|
||||
);
|
||||
|
||||
const handleSelect = (month?: Date) => {
|
||||
setSelectedMonth(month);
|
||||
onMonthSelect?.(month);
|
||||
};
|
||||
|
||||
if (dropdownCaption && (!fromDate || !toDate)) {
|
||||
console.warn("Using dropdownCaption required fromDate and toDate");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cl("navds-date__wrapper", className)}>
|
||||
<DateTranslationContextProvider translate={translate}>
|
||||
<DayPickerProvider
|
||||
initialProps={{
|
||||
locale: locale ? getLocaleFromString(locale) : langProviderLocale,
|
||||
selected: selected ?? selectedMonth,
|
||||
toDate,
|
||||
fromDate,
|
||||
month: selected ?? selectedMonth,
|
||||
}}
|
||||
>
|
||||
<div className="navds-date rdp-month">
|
||||
<SharedMonthProvider
|
||||
dropdownCaption={dropdownCaption}
|
||||
disabled={disabled}
|
||||
selected={selected ?? selectedMonth}
|
||||
onSelect={handleSelect}
|
||||
year={year}
|
||||
onYearChange={onYearChange}
|
||||
>
|
||||
<MonthCaption />
|
||||
<MonthSelector />
|
||||
</SharedMonthProvider>
|
||||
</div>
|
||||
</DayPickerProvider>
|
||||
</DateTranslationContextProvider>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default MonthPickerStandalone;
|
||||
+14
-13
@@ -1,14 +1,10 @@
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { useDateLocale } from "../../util/i18n/i18n.hooks";
|
||||
import { MonthPickerProps } from "../monthpicker/types";
|
||||
import { DateInputProps } from "../parts/DateInput";
|
||||
import {
|
||||
formatDateForInput,
|
||||
getLocaleFromString,
|
||||
isMatch,
|
||||
isValidDate,
|
||||
parseDate,
|
||||
} from "../utils";
|
||||
import { dateMatchModifiers } from "react-day-picker";
|
||||
import { useDateLocale } from "../../../util/i18n/i18n.hooks";
|
||||
import { DateInputProps } from "../../Date.Input";
|
||||
import { getLocaleFromString } from "../../Date.locale";
|
||||
import { formatDateForInput, isValidDate, parseDate } from "../../date-utils";
|
||||
import { MonthPickerProps } from "../MonthPicker.types";
|
||||
|
||||
export interface UseMonthPickerOptions
|
||||
extends Pick<
|
||||
@@ -267,11 +263,14 @@ export const useMonthpicker = (
|
||||
const isBefore = getIsBefore({ fromDate, date: month });
|
||||
const isAfter = getIsAfter({ toDate, date: month });
|
||||
|
||||
if (!isValidDate(month) || (disabled && isMatch(month, disabled))) {
|
||||
if (
|
||||
!isValidDate(month) ||
|
||||
(disabled && dateMatchModifiers(month, disabled))
|
||||
) {
|
||||
updateMonth(undefined);
|
||||
updateValidation({
|
||||
isInvalid: isValidDate(month),
|
||||
isDisabled: disabled && isMatch(month, disabled),
|
||||
isDisabled: disabled && dateMatchModifiers(month, disabled),
|
||||
isValidMonth: false,
|
||||
isEmpty: !e.target.value,
|
||||
isBefore: isBefore ?? false,
|
||||
@@ -283,7 +282,9 @@ export const useMonthpicker = (
|
||||
if (
|
||||
isAfter ||
|
||||
isBefore ||
|
||||
(fromDate && toDate && !isMatch(month, [{ from: fromDate, to: toDate }]))
|
||||
(fromDate &&
|
||||
toDate &&
|
||||
!dateMatchModifiers(month, [{ from: fromDate, to: toDate }]))
|
||||
) {
|
||||
updateMonth(undefined);
|
||||
updateValidation({
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
export { useMonthpicker, type MonthValidationT } from "../hooks";
|
||||
export { useMonthpicker, type MonthValidationT } from "./hooks/useMonthPicker";
|
||||
export { default as MonthPicker } from "./MonthPicker";
|
||||
export { type MonthPickerProps } from "./types";
|
||||
export { type MonthPickerProps } from "./MonthPicker.types";
|
||||
export {
|
||||
MonthPickerStandalone,
|
||||
type MonthPickerStandaloneProps,
|
||||
} from "./MonthPickerStandalone";
|
||||
export { MonthPickerInput } from "../parts/DateInput";
|
||||
} from "./parts/MonthPicker.Standalone";
|
||||
export { MonthPickerInput } from "../Date.Input";
|
||||
@@ -3,10 +3,10 @@ import { expect, userEvent, within } from "@storybook/test";
|
||||
import { setYear } from "date-fns";
|
||||
import React, { useId, useState } from "react";
|
||||
import { Button } from "../../button";
|
||||
import { useMonthpicker } from "../hooks";
|
||||
import { DateInputProps } from "../parts/DateInput";
|
||||
import { DateInputProps } from "../Date.Input";
|
||||
import MonthPicker from "./MonthPicker";
|
||||
import { MonthPickerProps } from "./types";
|
||||
import { MonthPickerProps } from "./MonthPicker.types";
|
||||
import { useMonthpicker } from "./hooks/useMonthPicker";
|
||||
|
||||
export default {
|
||||
title: "ds-react/Monthpicker",
|
||||
@@ -25,11 +25,12 @@ export const Default: StoryFn<{
|
||||
const { inputProps, monthpickerProps } = useMonthpicker({
|
||||
disabled: [new Date("Apr 1 2022")],
|
||||
locale: props.locale,
|
||||
onMonthChange: console.log,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ height: "20rem" }}>
|
||||
<MonthPicker {...monthpickerProps} onMonthSelect={console.log}>
|
||||
<MonthPicker {...monthpickerProps}>
|
||||
<MonthPicker.Input
|
||||
label="Velg måned"
|
||||
variant="monthpicker"
|
||||
|
||||
+21
-10
@@ -7,9 +7,9 @@ import {
|
||||
setYear,
|
||||
} from "date-fns";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useDayPicker } from "react-day-picker";
|
||||
import { useSharedMonthContext } from "../context";
|
||||
import { dateIsInCurrentMonth, isMatch, nextEnabled } from "../utils";
|
||||
import { dateIsInCurrentMonth, isMatch } from "../../date-utils";
|
||||
import { useMonthPickerContext } from "../MonthPicker.context";
|
||||
import { nextEnabled } from "../MonthPicker.util";
|
||||
|
||||
interface MonthType {
|
||||
month: Date;
|
||||
@@ -47,10 +47,18 @@ export const MonthButton = ({
|
||||
setTabRoot,
|
||||
}: MonthType) => {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const { hasDropdown, selected, onSelect, year, toYear, disabled } =
|
||||
useSharedMonthContext();
|
||||
|
||||
const { fromDate, toDate, locale } = useDayPicker();
|
||||
const {
|
||||
fromDate,
|
||||
toDate,
|
||||
locale,
|
||||
selected,
|
||||
disabled,
|
||||
year,
|
||||
onYearChange,
|
||||
onMonthSelect,
|
||||
caption,
|
||||
} = useMonthPickerContext();
|
||||
const isSelected = selected && isSameMonth(month, selected);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -64,15 +72,18 @@ export const MonthButton = ({
|
||||
isMatch(setYear(month, year.getFullYear()), disabled) ||
|
||||
disableMonth(month, fromDate, toDate);
|
||||
|
||||
const isThisMonth = dateIsInCurrentMonth(month, year);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
onClick={() => onSelect(isSelected ? undefined : month)}
|
||||
onClick={() => onMonthSelect?.(isSelected ? undefined : month)}
|
||||
disabled={isDisabled}
|
||||
aria-pressed={!!isSelected}
|
||||
data-current-month={isThisMonth}
|
||||
className={cl("navds-date__month-button", {
|
||||
"rdp-day_today": dateIsInCurrentMonth(month, year),
|
||||
"rdp-day_today": isThisMonth,
|
||||
"rdp-day_selected": isSelected,
|
||||
"rdp-day_disabled": isDisabled,
|
||||
})}
|
||||
@@ -87,9 +98,9 @@ export const MonthButton = ({
|
||||
e.key,
|
||||
disabled,
|
||||
month,
|
||||
toYear,
|
||||
onYearChange,
|
||||
year,
|
||||
hasDropdown,
|
||||
caption === "dropdown",
|
||||
fromDate,
|
||||
toDate,
|
||||
);
|
||||
+20
-21
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
format,
|
||||
isAfter,
|
||||
isBefore,
|
||||
setYear,
|
||||
@@ -6,25 +7,21 @@ import {
|
||||
startOfYear,
|
||||
} from "date-fns";
|
||||
import React from "react";
|
||||
import { useDayPicker } from "react-day-picker";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons";
|
||||
import { Button } from "../../button";
|
||||
import { Select } from "../../form/select";
|
||||
import { useDateTranslationContext, useSharedMonthContext } from "../context";
|
||||
import { Button } from "../../../button";
|
||||
import { Select } from "../../../form/select";
|
||||
import { useDateTranslationContext } from "../../Date.locale";
|
||||
import { useMonthPickerContext } from "../MonthPicker.context";
|
||||
|
||||
const MonthPickerCaption = () => {
|
||||
const { fromDate, toDate, locale, year, onYearChange, caption } =
|
||||
useMonthPickerContext();
|
||||
|
||||
export const MonthCaption = () => {
|
||||
const {
|
||||
fromDate,
|
||||
toDate,
|
||||
formatters: { formatYearCaption },
|
||||
locale,
|
||||
} = useDayPicker();
|
||||
const { hasDropdown, year, toYear } = useSharedMonthContext();
|
||||
const translate = useDateTranslationContext().translate;
|
||||
|
||||
const years: Date[] = [];
|
||||
|
||||
if (hasDropdown && fromDate && toDate) {
|
||||
if (caption === "dropdown" && fromDate && toDate) {
|
||||
const fromYear = fromDate.getFullYear();
|
||||
const toDateYear = toDate.getFullYear();
|
||||
for (let currYear = fromYear; currYear <= toDateYear; currYear++) {
|
||||
@@ -37,12 +34,14 @@ export const MonthCaption = () => {
|
||||
years.sort((a, b) => b.getFullYear() - a.getFullYear());
|
||||
}
|
||||
|
||||
const handleYearChange = (event: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
toYear(setYear(startOfMonth(new Date()), Number(event.target.value)));
|
||||
const handleYearChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
onYearChange(setYear(startOfMonth(new Date()), Number(event.target.value)));
|
||||
};
|
||||
|
||||
const handleButtonClick = (val: number) => {
|
||||
const newYear = Number(year.getFullYear() + val);
|
||||
toYear(setYear(year, newYear));
|
||||
|
||||
onYearChange(setYear(year, newYear));
|
||||
};
|
||||
|
||||
const disablePreviousYear = () => {
|
||||
@@ -64,11 +63,11 @@ export const MonthCaption = () => {
|
||||
disabled={disablePreviousYear()}
|
||||
onClick={() => handleButtonClick(-1)}
|
||||
icon={<ArrowLeftIcon title={translate("goToPreviousYear")} />}
|
||||
variant="tertiary"
|
||||
variant="tertiary-neutral"
|
||||
type="button"
|
||||
/>
|
||||
|
||||
{hasDropdown ? (
|
||||
{caption === "dropdown" ? (
|
||||
<Select
|
||||
label={translate("year")}
|
||||
hideLabel
|
||||
@@ -78,7 +77,7 @@ export const MonthCaption = () => {
|
||||
>
|
||||
{years.map((yearOpt) => (
|
||||
<option key={yearOpt.getFullYear()} value={yearOpt.getFullYear()}>
|
||||
{formatYearCaption(yearOpt, { locale })}
|
||||
{format(yearOpt, "yyyy", { locale })}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
@@ -92,11 +91,11 @@ export const MonthCaption = () => {
|
||||
disabled={disableNextYear()}
|
||||
onClick={() => handleButtonClick(1)}
|
||||
icon={<ArrowRightIcon title={translate("goToNextYear")} />}
|
||||
variant="tertiary"
|
||||
variant="tertiary-neutral"
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonthCaption;
|
||||
export { MonthPickerCaption };
|
||||
@@ -0,0 +1,86 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import { useDateLocale, useI18n } from "../../../util/i18n/i18n.hooks";
|
||||
import {
|
||||
DateTranslationContextProvider,
|
||||
getLocaleFromString,
|
||||
getTranslations,
|
||||
} from "../../Date.locale";
|
||||
import { MonthPickerProvider } from "../MonthPicker.context";
|
||||
import { MonthPickerProps } from "../MonthPicker.types";
|
||||
import { MonthPickerCaption } from "./MonthPicker.Caption";
|
||||
import { MonthPickerTable } from "./MonthPicker.Table";
|
||||
|
||||
export interface MonthPickerStandaloneProps
|
||||
extends Omit<
|
||||
MonthPickerProps,
|
||||
"open" | "onClose" | "onOpenToggle" | "wrapperClassName" | "strategy"
|
||||
> {
|
||||
/**
|
||||
* Monthpicker classname
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type MonthPickerStandaloneType = React.ForwardRefExoticComponent<
|
||||
MonthPickerStandaloneProps & React.RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
export const MonthPickerStandalone = forwardRef<
|
||||
HTMLDivElement,
|
||||
MonthPickerStandaloneProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
dropdownCaption = false,
|
||||
fromDate,
|
||||
toDate,
|
||||
disabled = [],
|
||||
selected,
|
||||
className,
|
||||
locale,
|
||||
translations,
|
||||
onMonthSelect,
|
||||
defaultSelected,
|
||||
year,
|
||||
onYearChange,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const translate = useI18n(
|
||||
"DatePicker",
|
||||
translations,
|
||||
getTranslations(locale),
|
||||
);
|
||||
const langProviderLocale = useDateLocale();
|
||||
|
||||
if (dropdownCaption && (!fromDate || !toDate)) {
|
||||
console.warn("Using dropdownCaption required fromDate and toDate");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cl("navds-date__wrapper", className)}>
|
||||
<DateTranslationContextProvider translate={translate}>
|
||||
<MonthPickerProvider
|
||||
dropdownCaption={dropdownCaption}
|
||||
defaultSelected={defaultSelected}
|
||||
selected={selected}
|
||||
disabled={disabled}
|
||||
fromDate={fromDate}
|
||||
toDate={toDate}
|
||||
year={year}
|
||||
onYearChange={onYearChange}
|
||||
onMonthSelect={onMonthSelect}
|
||||
locale={locale ? getLocaleFromString(locale) : langProviderLocale}
|
||||
>
|
||||
<div className="navds-date rdp-month">
|
||||
<MonthPickerCaption />
|
||||
<MonthPickerTable />
|
||||
</div>
|
||||
</MonthPickerProvider>
|
||||
</DateTranslationContextProvider>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
+7
-7
@@ -1,9 +1,9 @@
|
||||
import { isSameMonth, setMonth, setYear, startOfMonth } from "date-fns";
|
||||
import React, { useState } from "react";
|
||||
import { BodyShort } from "../../typography";
|
||||
import { useSharedMonthContext } from "../context";
|
||||
import { isMatch } from "../utils";
|
||||
import MonthButton from "./MonthButton";
|
||||
import { BodyShort } from "../../../typography";
|
||||
import { isMatch } from "../../date-utils";
|
||||
import { useMonthPickerContext } from "../MonthPicker.context";
|
||||
import MonthButton from "./MonthPicker.Button";
|
||||
|
||||
const getAllMonths = () => {
|
||||
const months: Date[] = [];
|
||||
@@ -14,10 +14,10 @@ const getAllMonths = () => {
|
||||
return months;
|
||||
};
|
||||
|
||||
export const MonthSelector = () => {
|
||||
const MonthPickerTable = () => {
|
||||
const [focus, setFocus] = useState<Date>();
|
||||
|
||||
const { selected, year, disabled } = useSharedMonthContext();
|
||||
const { selected, year, disabled } = useMonthPickerContext();
|
||||
|
||||
const months: Date[] = getAllMonths();
|
||||
|
||||
@@ -82,4 +82,4 @@ export const MonthSelector = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default MonthSelector;
|
||||
export { MonthPickerTable };
|
||||
@@ -1,22 +0,0 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { dateIsInCurrentMonth } from "..";
|
||||
|
||||
const selectedDate = new Date("Feb 1 1994");
|
||||
|
||||
describe("Returns if date is in current month", () => {
|
||||
test("Date should be within current month (true)", () => {
|
||||
expect(dateIsInCurrentMonth(new Date(), new Date())).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Date should not be within current month (false)", () => {
|
||||
expect(
|
||||
dateIsInCurrentMonth(new Date("Sep 2 2021"), selectedDate),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
dateIsInCurrentMonth(new Date("Sep 1 2021"), selectedDate),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
dateIsInCurrentMonth(new Date("Sep 30 2021"), selectedDate),
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { getMonths, getYears } from "..";
|
||||
|
||||
describe("Extracts correct months", () => {
|
||||
test("March - October (8)", () => {
|
||||
const t = {
|
||||
start: new Date(2019, 2, 22),
|
||||
end: new Date(2019, 9, 22),
|
||||
current: new Date(2019, 6, 22),
|
||||
res: 8,
|
||||
};
|
||||
expect(getMonths(t.start, t.end, t.current).length).toEqual(t.res);
|
||||
});
|
||||
|
||||
test("Only first 7 months", () => {
|
||||
const t = {
|
||||
start: new Date(2019, 0, 22),
|
||||
end: new Date(2022, 7, 22),
|
||||
current: new Date(2022, 6, 22),
|
||||
res: 8,
|
||||
};
|
||||
expect(getMonths(t.start, t.end, t.current).length).toEqual(t.res);
|
||||
});
|
||||
|
||||
test("All of 2019 when only 2019 avaliable (12)", () => {
|
||||
const t = {
|
||||
start: new Date(2019, 0, 22),
|
||||
end: new Date(2019, 11, 22),
|
||||
current: new Date(2019, 6, 22),
|
||||
res: 12,
|
||||
};
|
||||
expect(getMonths(t.start, t.end, t.current).length).toEqual(t.res);
|
||||
});
|
||||
test("All of 2019 when start + current is 2019 and end is later(12)", () => {
|
||||
const t = {
|
||||
start: new Date(2019, 0, 22),
|
||||
end: new Date(2022, 7, 22),
|
||||
current: new Date(2019, 6, 22),
|
||||
res: 12,
|
||||
};
|
||||
expect(getMonths(t.start, t.end, t.current).length).toEqual(t.res);
|
||||
});
|
||||
test("All of 2020 when between 2019 and 2022 (12)", () => {
|
||||
const t = {
|
||||
start: new Date(2019, 0, 22),
|
||||
end: new Date(2022, 7, 22),
|
||||
current: new Date(2020, 6, 22),
|
||||
res: 12,
|
||||
};
|
||||
expect(getMonths(t.start, t.end, t.current).length).toEqual(t.res);
|
||||
});
|
||||
test("Tail part of 2019, including starting month (12)", () => {
|
||||
const t = {
|
||||
start: new Date(2019, 5, 22),
|
||||
end: new Date(2022, 7, 22),
|
||||
current: new Date(2019, 6, 22),
|
||||
res: 7,
|
||||
};
|
||||
expect(getMonths(t.start, t.end, t.current).length).toEqual(t.res);
|
||||
});
|
||||
test("End part of 2022, including starting month (12)", () => {
|
||||
const t = {
|
||||
start: new Date(2019, 5, 22),
|
||||
end: new Date(2022, 7, 22),
|
||||
current: new Date(2022, 7, 22),
|
||||
res: 8,
|
||||
};
|
||||
expect(getMonths(t.start, t.end, t.current).length).toEqual(t.res);
|
||||
});
|
||||
test("Adds out of range month to list (13)", () => {
|
||||
const t = {
|
||||
start: new Date(2019, 5, 22),
|
||||
end: new Date(2022, 7, 22),
|
||||
current: new Date(2022, 9, 22),
|
||||
res: 9,
|
||||
};
|
||||
expect(getMonths(t.start, t.end, t.current).length).toEqual(t.res);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Extracts correct years", () => {
|
||||
test("Same year (1)", () => {
|
||||
const t = {
|
||||
start: new Date(2019, 2, 22),
|
||||
end: new Date(2019, 9, 22),
|
||||
current: 2019,
|
||||
res: 1,
|
||||
};
|
||||
expect(getYears(t.start, t.end, t.current).length).toEqual(t.res);
|
||||
});
|
||||
|
||||
test("Multiple years (11)", () => {
|
||||
const t = {
|
||||
start: new Date(2019, 2, 22),
|
||||
end: new Date(2029, 9, 22),
|
||||
current: 2022,
|
||||
res: 11,
|
||||
};
|
||||
expect(getYears(t.start, t.end, t.current).length).toEqual(t.res);
|
||||
});
|
||||
|
||||
test("Displaymonth outside range: after (11)", () => {
|
||||
const t = {
|
||||
start: new Date(2019, 2, 22),
|
||||
end: new Date(2029, 9, 22),
|
||||
current: 2040,
|
||||
res: 12,
|
||||
};
|
||||
expect(getYears(t.start, t.end, t.current).length).toEqual(t.res);
|
||||
});
|
||||
|
||||
test("Displaymonth outside range: before (11)", () => {
|
||||
const t = {
|
||||
start: new Date(2019, 2, 22),
|
||||
end: new Date(2029, 9, 22),
|
||||
current: 2001,
|
||||
res: 12,
|
||||
};
|
||||
expect(getYears(t.start, t.end, t.current).length).toEqual(t.res);
|
||||
});
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
import { isSameYear } from "date-fns";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { getInitialYear } from "..";
|
||||
|
||||
describe("Returns initial year for monthpicker", () => {
|
||||
test("Default selected should be valid (true)", () => {
|
||||
expect(
|
||||
isSameYear(
|
||||
new Date("Oct 4 2021"),
|
||||
getInitialYear({
|
||||
defaultMonth: new Date("Aug 4 2021"),
|
||||
}),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
test("Default selected should not be valid (false)", () => {
|
||||
expect(
|
||||
isSameYear(
|
||||
new Date("Oct 4 2021"),
|
||||
getInitialYear({
|
||||
defaultMonth: new Date("Aug 4 2020"),
|
||||
}),
|
||||
),
|
||||
).toBeFalsy();
|
||||
});
|
||||
test("Default selected should be valid with fromDate", () => {
|
||||
expect(
|
||||
isSameYear(
|
||||
new Date("Oct 4 2021"),
|
||||
getInitialYear({
|
||||
defaultMonth: new Date("Aug 4 2021"),
|
||||
/* toDate: new Date("Oct 4 2021"), */
|
||||
fromDate: new Date("Oct 4 2021"),
|
||||
}),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
test("Default selected should be valid with toDate", () => {
|
||||
expect(
|
||||
isSameYear(
|
||||
new Date("Oct 4 2021"),
|
||||
getInitialYear({
|
||||
defaultMonth: new Date("Aug 4 2021"),
|
||||
toDate: new Date("Jun 4 2021"),
|
||||
}),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
test("Default selected should be moved to toDate", () => {
|
||||
expect(
|
||||
isSameYear(
|
||||
new Date("Oct 4 2020"),
|
||||
getInitialYear({
|
||||
defaultMonth: new Date("Aug 4 2021"),
|
||||
toDate: new Date("Jun 4 2020"),
|
||||
}),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
test("Default selected should be moved to fromDate", () => {
|
||||
expect(
|
||||
isSameYear(
|
||||
new Date("Oct 4 2022"),
|
||||
getInitialYear({
|
||||
defaultMonth: new Date("Aug 4 2021"),
|
||||
fromDate: new Date("Jun 4 2022"),
|
||||
}),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
test("Default selected should be moved to fromDate, not toDate", () => {
|
||||
expect(
|
||||
isSameYear(
|
||||
new Date("Oct 4 2022"),
|
||||
getInitialYear({
|
||||
defaultMonth: new Date("Aug 4 2021"),
|
||||
toDate: new Date("Oct 4 2023"),
|
||||
fromDate: new Date("Jun 4 2022"),
|
||||
}),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,116 +0,0 @@
|
||||
/* https://github.com/gpbl/react-day-picker/blob/main/src/components/Table/utils/getMonthWeeks.test.ts */
|
||||
import { enGB, nb } from "date-fns/locale";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { getMonthWeeks } from "../get-month-weeks";
|
||||
|
||||
describe('when using the "nB" locale', () => {
|
||||
const locale = nb;
|
||||
describe("when getting the weeks for January 2022", () => {
|
||||
const weeks = getMonthWeeks(new Date(2022, 0), { locale });
|
||||
test("the first week should be the last of the previous year", () => {
|
||||
const weekNumbers = weeks.map((week) => week.weekNumber);
|
||||
expect(weekNumbers[0]).toEqual(52);
|
||||
});
|
||||
test("the first week should contain days from previous year", () => {
|
||||
expect(weeks[0].dates.map((date) => date.getDate())).toEqual([
|
||||
27, 28, 29, 30, 31, 1, 2,
|
||||
]);
|
||||
});
|
||||
test("the last week should be the last of January", () => {
|
||||
const weekNumbers = weeks.map((week) => week.weekNumber);
|
||||
expect(weekNumbers[weekNumbers.length - 1]).toEqual(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using the "enGB" locale', () => {
|
||||
const locale = enGB;
|
||||
describe("when using fixed weeks", () => {
|
||||
const useFixedWeeks = true;
|
||||
describe("when getting the weeks for December 2022", () => {
|
||||
const weeks = getMonthWeeks(new Date(2022, 11), {
|
||||
useFixedWeeks,
|
||||
locale,
|
||||
});
|
||||
test("should return 48 - 1 week numbers", () => {
|
||||
const weekNumbers = weeks.map((week) => week.weekNumber);
|
||||
const expectedResult = [48, 49, 50, 51, 52, 1];
|
||||
expect(weekNumbers).toEqual(expectedResult);
|
||||
});
|
||||
test("the last week should be the one in the next year", () => {
|
||||
const lastWeek = weeks[weeks.length - 1];
|
||||
const lastWeekDates = lastWeek.dates.map((date) => date.getDate());
|
||||
const expectedResult = [2, 3, 4, 5, 6, 7, 8];
|
||||
expect(lastWeekDates).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
describe("when getting the weeks for December 2021", () => {
|
||||
const weeks = getMonthWeeks(new Date(2021, 11), {
|
||||
useFixedWeeks,
|
||||
locale,
|
||||
});
|
||||
test("should return 48 - 1 week numbers", () => {
|
||||
const weekNumbers = weeks.map((week) => week.weekNumber);
|
||||
const expectedResult = [48, 49, 50, 51, 52, 1];
|
||||
expect(weekNumbers).toEqual(expectedResult);
|
||||
});
|
||||
test("week 1 contains the first day of the new year", () => {
|
||||
expect(weeks[4].dates.map((date) => date.getDate())).toEqual([
|
||||
27, 28, 29, 30, 31, 1, 2,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using the "nb" locale', () => {
|
||||
const locale = nb;
|
||||
describe("when using fixed weeks", () => {
|
||||
const useFixedWeeks = true;
|
||||
describe("when getting the weeks for December 2022", () => {
|
||||
const weeks = getMonthWeeks(new Date(2022, 11), {
|
||||
useFixedWeeks,
|
||||
locale,
|
||||
});
|
||||
test("should return 48 - 1 week numbers", () => {
|
||||
const weekNumbers = weeks.map((week) => week.weekNumber);
|
||||
const expectedResult = [48, 49, 50, 51, 52, 1];
|
||||
expect(weekNumbers).toEqual(expectedResult);
|
||||
});
|
||||
test("the last week should be the one in the next year", () => {
|
||||
const lastWeek = weeks[weeks.length - 1];
|
||||
const lastWeekDates = lastWeek.dates.map((date) => date.getDate());
|
||||
const expectedResult = [2, 3, 4, 5, 6, 7, 8];
|
||||
expect(lastWeekDates).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
describe("when getting the weeks for December 2021", () => {
|
||||
const weeks = getMonthWeeks(new Date(2021, 11), {
|
||||
useFixedWeeks,
|
||||
locale,
|
||||
});
|
||||
test("should return 48 - 1 week numbers", () => {
|
||||
const weekNumbers = weeks.map((week) => week.weekNumber);
|
||||
const expectedResult = [48, 49, 50, 51, 52, 1];
|
||||
expect(weekNumbers).toEqual(expectedResult);
|
||||
});
|
||||
test("week 1 contains the first day of the new year", () => {
|
||||
expect(weeks[4].dates.map((date) => date.getDate())).toEqual([
|
||||
27, 28, 29, 30, 31, 1, 2,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMonthWeeks should calculate week-number corectly", () => {
|
||||
const locale = nb;
|
||||
describe("when getting the weeks for September 2022", () => {
|
||||
const date = new Date(2022, 8);
|
||||
const weeks = getMonthWeeks(date, { locale });
|
||||
test("the last week should have number 39", () => {
|
||||
const weekNumbers = weeks.map((week) => week.weekNumber);
|
||||
expect(weekNumbers[weekNumbers.length - 1]).toEqual(39);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loaded 100 of 158 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user