Compare commits

..
Author SHA1 Message Date
github-actions[bot]andgithub-actions[bot] 2a4465a051 Version Packages (#3603)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-20 16:24:29 +01:00
Ken b77dbe0cad 🐛 Datepicker now checks that from/to dates are actually another day (#3602)
* 🐛 Datepicker now checks that from/to dates are actually another day

* 🐛 Wraps fromY/to dates in startOfDay to handle timezone differences
2025-02-20 16:17:08 +01:00
github-actions[bot]andgithub-actions[bot] 78fc16c1a9 Version Packages (#3593)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-19 12:01:40 +01:00
Ken 5937d085c0 ✨ Spacer-tokens (#3587) 2025-02-19 09:51:09 +01:00
Ken 2f248a65f7 ✨ Added docs-config for tokens 2025-02-17 11:20:03 +01:00
KenandVegard Haugstvedt 08e134ef3c [Darkside] ✨ Re-added arrow to Tooltip (#3589)
* ✨ Re-added arrow to Tooltip

* Update .storybook/docs/PilotMigration.mdx

Co-authored-by: Vegard Haugstvedt <it.vegard@gmail.com>

---------

Co-authored-by: Vegard Haugstvedt <it.vegard@gmail.com>
2025-02-17 10:32:02 +01:00
Ken 821343b67d [Datepicker] Bumped react-day-picker to v9. (#3525)
* 📝 Bumped date-fns to v4

* TEMP UPDATES

* TEMP CSS UPDATES

* ✨ New Caption component for datepicker

* ♻️ Extract util

* ♻️ Refactor standalone datepicker

* ♻️ Refactor dialog datepicker

* ♻️ Migrated weekday-click

* ♻️ Migrated mobile-view week-click

* ⚰️ Removed old code

* 🚚 Re-strucutre project

* 🐛 Fix imports

* ♻️ New MonthPicker context

* ♻️ Replace use to MonthPickerContext

* ♻️ Migrated standalone monthpicker

* ♻️ Remove react-day-picker connection for utils

* 🚚 Re-structure MonthPicker

* 📝 rename context

* 🚚 Re-structure DatePicker

* 🐛 Fix imports and nb

* ♻️ Parts restructure

* 🚚 Move inputcontext

* 🚚 Co-locate date-spesific context

* 🚚 Co-locate date-hooks

* 🐛 Update missing import-updates

* 🐛 Use new input-context in modal, popover

* 🚚 Moved new utils into date-utils

* ♻️ simplified calendar-range logic

* 🐛 Fix test-runs

* 🧪 Storybook tests for monthpicker

* 🧪 Storybook tests for monthpicker

* 💄 Lock datepicker dialog to top of screen

* 🐛 Don't re-open modal

* 🐛 Avoid double open-toggle

* 🧪 Storybook tests for DatePicker

* ♻️ Use local dateRange-handler

* 🐛 month-update caused custom-components to re-render

* 🐛 casings

* 🐛 casings 2

* 🐛 casings 3

* 🐛 casings 4

* 🧪 Added tests for legacy dom-structure

* 🐛 Resolved re-render causing tests to fail

* 🧪 More datepicker tests, daypicker now requires us to handle focus

* 📝 Changeset

* Update @navikt/core/react/src/date/datepicker/parts/DatePicker.RDP.tsx

* 🐛 Added weekend-className for backwards compat support

* 📝 yarn lock sync

* 🐛 Allow defaultMonth-prop in Datepicker
2025-02-17 09:46:40 +01:00
Ken 19b42daa69 🐛 New 'god-praksis' artikles were hidden from frontpage unless referenced to 2025-02-14 14:59:22 +01:00
github-actions[bot]andgithub-actions[bot] 09c44edccb Version Packages (#3584)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-14 14:38:59 +01:00
Ken 2fab45f757 ✨ Added contrast-token to themable-roles 2025-02-14 14:16:51 +01:00
Ken 1ba5d9b5a2 [Darkside] Added support for ConfirmationPanel, Linkpanel, Panel and Dropdown. (#3581)
* ✨ Added ConfirmationPanel to darkside

* ✨ Added Panel support

* ✨ Added linkpanel support

* ✨ Added dropdown support

* 📝 changeset

* 🐛 Removed exception for now re-added CSS
2025-02-13 19:29:13 +01:00
Ken 5a749690c6 [Darkside] Updated darkside token-build (#3578)
* ✨ Added themed roles config to darkside

* ♻️ Rename breakpint config

* 🚚 Move files

* 🐛 Update renamed import

* 🐛 Build theme-files before readig them

* ♻️ Refactored color-tokens setup

* ♻️ Updated setup and imports

* ♻️ Hide unwanted tokenfiles

* 🐛 Update imports for refactored colors

* ♻️ Simplified token-build

* ♻️ Removed custom root build

* ♻️ Add cleanup-step

* 📢 Hide verbose warnings

* 🐛 Added --ax- prefix to global tokens

* 📝 Removed dark-theme tag
2025-02-13 19:28:34 +01:00
KenandJulian Nymark 2f7873d3e7 [Aksel.nav.no] ✨ Highlights 'darkside' sections in sidebar (#3582)
* ✨ Highlights 'darkside' sections in sidebar

* 🔥 removed test-code

* Update aksel.nav.no/website/components/website-modules/menu/Menu.tsx

---------

Co-authored-by: Julian Nymark <julian.nymark@nav.no>
2025-02-13 15:35:46 +01:00
Ken e5dc975c44 ⬅️ Revert use of class update 2025-02-13 14:03:01 +01:00
Ken 1cc24c2998 [Darkside] Added hook for renaming className-prefix from navds-> aksel- (#3585)
* ✨ MVP replacement function

* ✨ Updated className replace function
2025-02-13 13:48:19 +01:00
Ken 636eb05bf5 ✨ Icon-update (#3583) 2025-02-13 13:24:27 +01:00
Ken c6424eda70 [Darkside] Tailwind support (#3580)
* 📝 Test config

* ✨ Added prefix-support, export for npm

* 📝 Added changeset, removed test-config

* 🐛 Removed conveluted tests

* 📝 Added temp-docs

* 📝 Added tailwind version suffix to export
2025-02-13 09:22:20 +01:00
Ken 90f76498d6 🐛 Handle icons where variant is 'filled' instead of 'fill'. 2025-02-13 09:14:42 +01:00
Julian Nymark 6201d87a1a Add more info on how to update an article (#3579) 2025-02-12 14:53:51 +01:00
Ken 4dcb4efeea 🐛 Fix flaky e2e-tests that timed out because of network-updates 2025-02-12 14:52:55 +01:00
Ken 5586d42096 Darkside role theming refactor (#3575)
* ✨ Added themed roles config to darkside

* ♻️ Rename breakpint config

* 🚚 Move files

* 🐛 Update renamed import

* 🐛 Build theme-files before readig them

* ♻️ Refactored color-tokens setup

* ♻️ Updated setup and imports

* ♻️ Hide unwanted tokenfiles

* 🐛 Update imports for refactored colors

* 📝 Clarify tests

* 📝 config -> configs
2025-02-12 09:24:43 +01:00
Ken 94e3bd7191 [Darkside] Added role-theming (#3569)
* ✨ Added themed roles config to darkside

* ♻️ Rename breakpint config

* 🐛 Update renamed import

* 🐛 Build theme-files before readig them

* 🐛 Updated breakpoint-import in test-files
2025-02-11 13:57:31 +01:00
Ken 0d886b12bf 📝 Added demo-page to pilot doc 2025-02-10 10:04:40 +01:00
Vegard Haugstvedt dfbec997fc Add language selector in decorator for templates (#3568) 2025-02-07 14:27:02 +01:00
Julian Nymark 1ec34524e7 Pilot docs edits (#3567)
* pilot text edits

* pilots edits

* pilot doc edits

* pilot doc edits

* update pilot docs
2025-02-07 12:53:16 +01:00
Ken 36f84458a5 🐛 Add type-annotations to exported ThemeProps 2025-02-07 12:08:52 +01:00
Julian Nymark 2271072d1c Website: remove snow (#3566) 2025-02-07 10:54:51 +01:00
Ken 0c22de8c61 [Aksel.nav.no] Added Hello bar under header (#3561)
* ✨ Added hello-bar

* 📝 Updated texts

* 🔥 Removed demo-versions

* 💄 Added x-axis padding
2025-02-06 13:50:00 +01:00
github-actions[bot]andgithub-actions[bot] 1b20ad54de Version Packages (#3563)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-06 13:17:51 +01:00
Ken 8576400263 🐛 Added BoxNew to index-export 2025-02-06 12:59:01 +01:00
158 changed files with 4397 additions and 3016 deletions

No files matched your search

+11 -10
View File
@@ -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
+14 -5
View File
@@ -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")],
}
```
+13 -2
View File
@@ -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.
+16
View File
@@ -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
+1 -1
View File
@@ -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
+3
View File
@@ -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

+9
View File
@@ -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 -1
View File
@@ -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": {
+8
View File
@@ -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
+3 -3
View File
@@ -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",
+28
View File
@@ -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
+2 -2
View File
@@ -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",
+20
View File
@@ -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
+1 -1
View File
@@ -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", () => {
+27 -37
View File
@@ -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 {
+2 -1
View File
@@ -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);
}
+4 -4
View File
@@ -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);
}
+24 -4
View File
@@ -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
View File
@@ -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;
}
+2 -2
View File
@@ -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",
+50
View File
@@ -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
+6 -6
View File
@@ -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",
@@ -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 };
@@ -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,
@@ -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,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,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,
};
});
}
@@ -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,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { isMatch } from "..";
import { isMatch } from "./is-match";
const disabled = [
new Date("Sep 8 2022"),
@@ -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);
@@ -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
@@ -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>
);
},
@@ -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"));
});
});
@@ -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,
@@ -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 = () => {
@@ -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 };
@@ -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,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> {
/**
@@ -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;
@@ -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"
@@ -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,
);
@@ -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>
);
},
);
@@ -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