@vygruppen/spor-react 1.3.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/.turbo/turbo-build.log +12 -10
  2. package/CHANGELOG.md +25 -0
  3. package/dist/index.d.ts +6718 -27
  4. package/dist/index.js +14139 -140
  5. package/dist/index.mjs +13818 -27
  6. package/package.json +19 -31
  7. package/src/accordion/Accordion.test.tsx +20 -0
  8. package/src/accordion/Accordion.tsx +62 -0
  9. package/src/accordion/AccordionContext.tsx +27 -0
  10. package/src/accordion/Expandable.tsx +157 -0
  11. package/src/accordion/index.tsx +2 -0
  12. package/src/alert/AlertIcon.tsx +75 -0
  13. package/src/alert/BaseAlert.test.tsx +37 -0
  14. package/src/alert/BaseAlert.tsx +21 -0
  15. package/src/alert/ClosableAlert.test.tsx +37 -0
  16. package/src/alert/ClosableAlert.tsx +75 -0
  17. package/src/alert/ExpandableAlert.test.tsx +84 -0
  18. package/src/alert/ExpandableAlert.tsx +84 -0
  19. package/src/alert/StaticAlert.tsx +25 -0
  20. package/src/alert/index.tsx +3 -0
  21. package/src/button/Button.test.tsx +23 -0
  22. package/src/button/Button.tsx +162 -0
  23. package/src/button/ButtonGroup.tsx +43 -0
  24. package/src/button/CloseButton.tsx +63 -0
  25. package/src/button/FloatingActionButton.tsx +113 -0
  26. package/src/button/IconButton.tsx +63 -0
  27. package/src/button/index.tsx +5 -0
  28. package/src/card/Card.tsx +59 -0
  29. package/src/card/index.tsx +1 -0
  30. package/src/datepicker/Calendar.tsx +32 -0
  31. package/src/datepicker/CalendarCell.tsx +74 -0
  32. package/src/datepicker/CalendarGrid.tsx +76 -0
  33. package/src/datepicker/CalendarHeader.tsx +153 -0
  34. package/src/datepicker/CalendarNavigationButton.tsx +26 -0
  35. package/src/datepicker/CalendarTriggerButton.tsx +36 -0
  36. package/src/datepicker/DateField.tsx +51 -0
  37. package/src/datepicker/DatePicker.tsx +153 -0
  38. package/src/datepicker/DateRangePicker.tsx +165 -0
  39. package/src/datepicker/DateTimeSegment.tsx +56 -0
  40. package/src/datepicker/RangeCalendar.tsx +35 -0
  41. package/src/datepicker/StyledField.tsx +31 -0
  42. package/src/datepicker/TimeField.tsx +46 -0
  43. package/src/datepicker/TimePicker.test.tsx +74 -0
  44. package/src/datepicker/TimePicker.tsx +196 -0
  45. package/src/datepicker/index.tsx +4 -0
  46. package/src/datepicker/utils.ts +33 -0
  47. package/src/i18n/index.tsx +38 -0
  48. package/src/image/index.tsx +2 -0
  49. package/src/index.tsx +25 -26
  50. package/src/input/CardSelect.tsx +165 -0
  51. package/src/input/Checkbox.tsx +24 -0
  52. package/src/input/CheckboxGroup.tsx +43 -0
  53. package/src/input/ChoiceChip.tsx +102 -0
  54. package/src/input/Dialog.tsx +29 -0
  55. package/src/input/FormControl.tsx +11 -0
  56. package/src/input/FormErrorMessage.tsx +91 -0
  57. package/src/input/FormLabel.tsx +11 -0
  58. package/src/input/InfoSelect.tsx +209 -0
  59. package/src/input/Input.tsx +59 -0
  60. package/src/input/InputElement.tsx +45 -0
  61. package/src/input/ListBox.tsx +123 -0
  62. package/src/input/NativeSelect.tsx +38 -0
  63. package/src/input/PasswordInput.tsx +70 -0
  64. package/src/input/Popover.tsx +70 -0
  65. package/src/input/Radio.tsx +34 -0
  66. package/src/input/RadioGroup.tsx +47 -0
  67. package/src/input/SearchInput.tsx +89 -0
  68. package/src/input/Switch.tsx +40 -0
  69. package/src/input/Textarea.tsx +98 -0
  70. package/src/input/index.tsx +20 -0
  71. package/src/layout/Divider.tsx +26 -0
  72. package/src/layout/Stack.tsx +42 -0
  73. package/src/layout/index.tsx +28 -0
  74. package/src/linjetag/InfoTag.tsx +54 -0
  75. package/src/linjetag/LineIcon.tsx +44 -0
  76. package/src/linjetag/TravelTag.tsx +121 -0
  77. package/src/linjetag/icons.tsx +80 -0
  78. package/src/linjetag/index.tsx +3 -0
  79. package/src/linjetag/types.d.ts +24 -0
  80. package/src/link/TextLink.tsx +45 -0
  81. package/src/link/index.tsx +1 -0
  82. package/src/loader/ClientOnly.tsx +29 -0
  83. package/src/loader/ColorInlineLoader.tsx +27 -0
  84. package/src/loader/ColorSpinner.tsx +44 -0
  85. package/src/loader/ContentLoader.tsx +27 -0
  86. package/src/loader/DarkFullScreenLoader.tsx +23 -0
  87. package/src/loader/DarkInlineLoader.tsx +25 -0
  88. package/src/loader/DarkSpinner.tsx +43 -0
  89. package/src/loader/LightFullScreenLoader.tsx +23 -0
  90. package/src/loader/LightInlineLoader.tsx +25 -0
  91. package/src/loader/LightSpinner.tsx +41 -0
  92. package/src/loader/Lottie.tsx +10 -0
  93. package/src/loader/ProgressBar.tsx +128 -0
  94. package/src/loader/ProgressLoader.tsx +140 -0
  95. package/src/loader/Skeleton.tsx +16 -0
  96. package/src/loader/SkeletonCircle.tsx +13 -0
  97. package/src/loader/SkeletonText.tsx +10 -0
  98. package/src/loader/index.tsx +14 -0
  99. package/src/loader/useHydrated.tsx +34 -0
  100. package/src/loader/useRotatingLabel.tsx +22 -0
  101. package/src/logo/VyLogo.tsx +101 -0
  102. package/src/logo/index.tsx +1 -0
  103. package/src/media-controller/JumpButton.tsx +69 -0
  104. package/src/media-controller/PlayPauseButton.tsx +67 -0
  105. package/src/media-controller/SkipButton.tsx +66 -0
  106. package/src/media-controller/icons.tsx +80 -0
  107. package/src/media-controller/index.test.tsx +59 -0
  108. package/src/media-controller/index.tsx +3 -0
  109. package/src/modal/Drawer.tsx +122 -0
  110. package/src/modal/Modal.tsx +15 -0
  111. package/src/modal/ModalHeader.tsx +31 -0
  112. package/src/modal/SimpleDrawer.tsx +44 -0
  113. package/src/modal/index.tsx +4 -0
  114. package/src/popover/PopoverWizardBody.tsx +91 -0
  115. package/src/popover/SimplePopover.tsx +75 -0
  116. package/src/popover/WizardPopover.tsx +61 -0
  117. package/src/popover/index.tsx +23 -0
  118. package/src/provider/SporProvider.tsx +67 -0
  119. package/src/provider/index.tsx +1 -0
  120. package/src/stepper/Stepper.tsx +115 -0
  121. package/src/stepper/StepperContext.tsx +55 -0
  122. package/src/stepper/StepperStep.tsx +48 -0
  123. package/src/stepper/index.tsx +2 -0
  124. package/src/tab/Tabs.tsx +20 -0
  125. package/src/tab/index.tsx +9 -0
  126. package/src/table/Table.tsx +58 -0
  127. package/src/table/index.tsx +19 -0
  128. package/src/theme/components/accordion.ts +143 -0
  129. package/src/theme/components/alert.ts +59 -0
  130. package/src/theme/components/badge.ts +109 -0
  131. package/src/theme/components/button.ts +217 -0
  132. package/src/theme/components/card-select.ts +158 -0
  133. package/src/theme/components/card.ts +174 -0
  134. package/src/theme/components/checkbox.ts +90 -0
  135. package/src/theme/components/choice-chip.ts +79 -0
  136. package/src/theme/components/close-button.ts +56 -0
  137. package/src/theme/components/code.ts +17 -0
  138. package/src/theme/components/datepicker.ts +194 -0
  139. package/src/theme/components/drawer.ts +92 -0
  140. package/src/theme/components/fab.ts +111 -0
  141. package/src/theme/components/form-label.ts +17 -0
  142. package/src/theme/components/form.ts +27 -0
  143. package/src/theme/components/index.ts +34 -0
  144. package/src/theme/components/info-select.ts +91 -0
  145. package/src/theme/components/info-tag.ts +49 -0
  146. package/src/theme/components/input.ts +97 -0
  147. package/src/theme/components/line-icon.ts +121 -0
  148. package/src/theme/components/link.ts +155 -0
  149. package/src/theme/components/listbox.ts +52 -0
  150. package/src/theme/components/media-controller-button.ts +134 -0
  151. package/src/theme/components/modal.ts +93 -0
  152. package/src/theme/components/popover.ts +63 -0
  153. package/src/theme/components/radio.ts +64 -0
  154. package/src/theme/components/select.ts +52 -0
  155. package/src/theme/components/skeleton.ts +40 -0
  156. package/src/theme/components/stepper.ts +230 -0
  157. package/src/theme/components/switch.ts +227 -0
  158. package/src/theme/components/table.ts +163 -0
  159. package/src/theme/components/tabs.ts +282 -0
  160. package/src/theme/components/textarea.ts +14 -0
  161. package/src/theme/components/toast.ts +28 -0
  162. package/src/theme/components/travel-tag.ts +267 -0
  163. package/src/theme/font-faces.ts +66 -0
  164. package/src/theme/foundations/borders.ts +11 -0
  165. package/src/theme/foundations/breakpoints.ts +9 -0
  166. package/src/theme/foundations/colors.ts +10 -0
  167. package/src/theme/foundations/config.ts +5 -0
  168. package/src/theme/foundations/fontSizes.ts +29 -0
  169. package/src/theme/foundations/fontWeights.ts +5 -0
  170. package/src/theme/foundations/fonts.ts +7 -0
  171. package/src/theme/foundations/index.ts +14 -0
  172. package/src/theme/foundations/lineHeights.ts +5 -0
  173. package/src/theme/foundations/radii.ts +12 -0
  174. package/src/theme/foundations/shadows.ts +8 -0
  175. package/src/theme/foundations/sizes.ts +34 -0
  176. package/src/theme/foundations/spacing.ts +30 -0
  177. package/src/theme/foundations/textStyles.ts +60 -0
  178. package/src/theme/foundations/zIndices.ts +17 -0
  179. package/src/theme/index.ts +14 -0
  180. package/src/theme/utils/box-shadow-utils.ts +44 -0
  181. package/src/theme/utils/focus-utils.ts +16 -0
  182. package/src/toast/ActionToast.test.tsx +22 -0
  183. package/src/toast/ActionToast.tsx +28 -0
  184. package/src/toast/BaseToast.test.tsx +27 -0
  185. package/src/toast/BaseToast.tsx +75 -0
  186. package/src/toast/ClosableToast.test.tsx +17 -0
  187. package/src/toast/ClosableToast.tsx +40 -0
  188. package/src/toast/index.tsx +1 -0
  189. package/src/toast/useToast.tsx +99 -0
  190. package/src/typography/Badge.tsx +68 -0
  191. package/src/typography/Code.tsx +32 -0
  192. package/src/typography/Heading.tsx +32 -0
  193. package/src/typography/Text.tsx +26 -0
  194. package/src/typography/index.tsx +4 -0
  195. package/src/util/externals.tsx +23 -0
  196. package/src/util/index.tsx +1 -0
@@ -0,0 +1,74 @@
1
+ import { Box, useMultiStyleConfig } from "@chakra-ui/react";
2
+ import {
3
+ CalendarDate,
4
+ DateValue,
5
+ isSameMonth,
6
+ isToday,
7
+ } from "@internationalized/date";
8
+ import React, { useRef } from "react";
9
+ import { useCalendarCell } from "react-aria";
10
+ import { CalendarState, RangeCalendarState } from "react-stately";
11
+
12
+ type CalendarCellProps = {
13
+ state: CalendarState | RangeCalendarState;
14
+ date: CalendarDate;
15
+ currentMonth: DateValue;
16
+ };
17
+ export function CalendarCell({ state, date, currentMonth }: CalendarCellProps) {
18
+ const ref = useRef(null);
19
+ const { cellProps, buttonProps, isSelected, isDisabled, isUnavailable } =
20
+ useCalendarCell({ date }, state, ref);
21
+
22
+ const isOutsideMonth = !isSameMonth(currentMonth, date);
23
+ const styles = useMultiStyleConfig("Datepicker", {});
24
+
25
+ const stateProps: Record<string, any> = {};
26
+ if (isSelected) {
27
+ stateProps["data-selected"] = true;
28
+ }
29
+ if (isDisabled || isUnavailable) {
30
+ stateProps["data-disabled"] = true;
31
+ }
32
+ if (isToday(date, "Europe/Oslo")) {
33
+ stateProps["data-today"] = true;
34
+ }
35
+ if (isOutsideMonth) {
36
+ stateProps["data-unavailable"] = true;
37
+ }
38
+
39
+ return (
40
+ <Box
41
+ as="td"
42
+ {...cellProps}
43
+ textAlign="center"
44
+ sx={{
45
+ '&[aria-selected="true"] + [aria-selected="true"] > button': {
46
+ "&::before": {
47
+ content: '""',
48
+ display: "block",
49
+ width: "100%",
50
+ height: "100%",
51
+ backgroundColor: "darkTeal",
52
+ position: "absolute",
53
+ left: "-50%",
54
+ top: 0,
55
+ bottom: 0,
56
+ zIndex: -1,
57
+ },
58
+ },
59
+ }}
60
+ >
61
+ <Box
62
+ as="button"
63
+ type="button"
64
+ {...buttonProps}
65
+ {...stateProps}
66
+ ref={ref}
67
+ sx={styles.dateCell}
68
+ width="100%"
69
+ >
70
+ {date.day}
71
+ </Box>
72
+ </Box>
73
+ );
74
+ }
@@ -0,0 +1,76 @@
1
+ import { endOfMonth, getWeeksInMonth } from "@internationalized/date";
2
+ import React from "react";
3
+ import { AriaCalendarGridProps, useCalendarGrid } from "react-aria";
4
+ import { CalendarState, RangeCalendarState } from "react-stately";
5
+ import { Language, useTranslation } from "../i18n";
6
+ import { Text } from "../typography";
7
+ import { CalendarCell } from "./CalendarCell";
8
+ import { useCurrentLocale } from "./utils";
9
+
10
+ const weekDays: Record<Language, string[]> = {
11
+ nb: ["Ma", "Ti", "On", "To", "Fr", "Lø", "Sø"],
12
+ nn: ["Ma", "Ti", "On", "To", "Fr", "Lø", "Sø"],
13
+ sv: ["Må", "Ti", "On", "To", "Fr", "Lö", "Sö"],
14
+ en: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
15
+ };
16
+
17
+ type CalendarGridProps = AriaCalendarGridProps & {
18
+ state: CalendarState | RangeCalendarState;
19
+ offset?: { months?: number };
20
+ };
21
+ export function CalendarGrid({ state, offset = {} }: CalendarGridProps) {
22
+ const { language } = useTranslation();
23
+ const locale = useCurrentLocale();
24
+ const startDate = state.visibleRange.start.add(offset);
25
+ const endDate = endOfMonth(startDate);
26
+ const { gridProps, headerProps } = useCalendarGrid(
27
+ {
28
+ startDate,
29
+ endDate,
30
+ },
31
+ state
32
+ );
33
+
34
+ // Get the number of weeks in the month so we can render the proper number of rows.
35
+ const weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale);
36
+ const weeksInMonthRange = new Array(weeksInMonth).fill(0).map((_, i) => i);
37
+
38
+ return (
39
+ <table {...gridProps}>
40
+ <thead {...headerProps}>
41
+ <tr>
42
+ {weekDays[language].map((day, index) => (
43
+ <Text
44
+ as="th"
45
+ key={index}
46
+ color={index < 5 ? "darkGrey" : "greenHaze"}
47
+ variant="sm"
48
+ >
49
+ {day}
50
+ </Text>
51
+ ))}
52
+ </tr>
53
+ </thead>
54
+ <tbody>
55
+ {weeksInMonthRange.map((weekIndex) => (
56
+ <tr key={weekIndex}>
57
+ {state
58
+ .getDatesInWeek(weekIndex, startDate)
59
+ .map((date, dayIndex) =>
60
+ date ? (
61
+ <CalendarCell
62
+ key={dayIndex}
63
+ state={state}
64
+ date={date}
65
+ currentMonth={startDate}
66
+ />
67
+ ) : (
68
+ <td key={dayIndex} />
69
+ )
70
+ )}
71
+ </tr>
72
+ ))}
73
+ </tbody>
74
+ </table>
75
+ );
76
+ }
@@ -0,0 +1,153 @@
1
+ import { Flex } from "@chakra-ui/react";
2
+ import { getLocalTimeZone } from "@internationalized/date";
3
+ import {
4
+ ArrowLeftOutline24Icon,
5
+ ArrowRightOutline24Icon,
6
+ } from "@vygruppen/spor-icon-react";
7
+ import React from "react";
8
+ import { CalendarState, RangeCalendarState } from "react-stately";
9
+ import { createTexts, useTranslation } from "../i18n";
10
+ import { Heading } from "../typography";
11
+ import { CalendarNavigationButton } from "./CalendarNavigationButton";
12
+ import { useCurrentLocale } from "./utils";
13
+
14
+ type CalendarHeaderProps = {
15
+ state: CalendarState | RangeCalendarState;
16
+ title?: string;
17
+ showYearNavigation?: boolean;
18
+ };
19
+ export function CalendarHeader({
20
+ state,
21
+ showYearNavigation = false,
22
+ title,
23
+ }: CalendarHeaderProps) {
24
+ const locale = useCurrentLocale();
25
+ const monthFormatter = Intl.DateTimeFormat(locale, {
26
+ month: "long",
27
+ });
28
+ const jsDate = state.focusedDate.toDate(getLocalTimeZone());
29
+
30
+ const monthTitle = monthFormatter.format(jsDate);
31
+ const monthAndYearTitle = `${monthTitle} ${state.focusedDate.year}`;
32
+
33
+ const isPreviousYearDisabled = state.isInvalid(
34
+ state.visibleRange.start.subtract({ years: 1 })
35
+ );
36
+ const isNextYearDisabled = state.isInvalid(
37
+ state.visibleRange.start.add({ years: 1 })
38
+ );
39
+ const areAllOtherYearsDisabled = isPreviousYearDisabled && isNextYearDisabled;
40
+ const isYearPickerVisible = showYearNavigation && !areAllOtherYearsDisabled;
41
+
42
+ return (
43
+ <Flex alignItems="center" paddingBottom="4" justifyContent="space-between">
44
+ <CalendarNavigator
45
+ title={
46
+ title ? title : isYearPickerVisible ? monthTitle : monthAndYearTitle
47
+ }
48
+ unit="month"
49
+ onPrevious={() =>
50
+ state.setFocusedDate(state.focusedDate.subtract({ months: 1 }))
51
+ }
52
+ onNext={() =>
53
+ state.setFocusedDate(state.focusedDate.add({ months: 1 }))
54
+ }
55
+ isNextDisabled={!state.isPreviousVisibleRangeInvalid}
56
+ isPreviousDisabled={!state.isNextVisibleRangeInvalid}
57
+ />
58
+ {isYearPickerVisible && (
59
+ <CalendarNavigator
60
+ title={jsDate.getFullYear().toString()}
61
+ unit="year"
62
+ onPrevious={() =>
63
+ state.setFocusedDate(state.focusedDate.subtract({ years: 1 }))
64
+ }
65
+ onNext={() =>
66
+ state.setFocusedDate(state.focusedDate.add({ years: 1 }))
67
+ }
68
+ isPreviousDisabled={isPreviousYearDisabled}
69
+ isNextDisabled={isNextYearDisabled}
70
+ />
71
+ )}
72
+ </Flex>
73
+ );
74
+ }
75
+
76
+ const capitalize = (str: string = "") =>
77
+ str.charAt(0).toUpperCase() + str.slice(1);
78
+
79
+ type CalendarNavigatorProps = {
80
+ /** The unit of time you want to navigate with */
81
+ unit: "month" | "year";
82
+ /** The text in the middle */
83
+ title: string;
84
+ /** Callback for when you click back */
85
+ onPrevious: () => void;
86
+ /** Callback for when you click forward */
87
+ onNext: () => void;
88
+ isNextDisabled: boolean;
89
+ isPreviousDisabled: boolean;
90
+ };
91
+ export const CalendarNavigator = ({
92
+ title,
93
+ unit,
94
+ onPrevious,
95
+ isPreviousDisabled,
96
+ onNext,
97
+ isNextDisabled,
98
+ }: CalendarNavigatorProps) => {
99
+ const { t } = useTranslation();
100
+ return (
101
+ <Flex alignItems="center" flexGrow={1}>
102
+ <CalendarNavigationButton
103
+ onPress={onPrevious}
104
+ isDisabled={isPreviousDisabled}
105
+ icon={<ArrowLeftOutline24Icon />}
106
+ aria-label={`${t(texts.previous)} ${t(texts[unit])}`}
107
+ />
108
+ <Heading
109
+ as="div"
110
+ role="heading"
111
+ variant="sm"
112
+ fontWeight="bold"
113
+ flex="1"
114
+ textAlign="center"
115
+ >
116
+ {capitalize(title)}
117
+ </Heading>
118
+ <CalendarNavigationButton
119
+ onPress={onNext}
120
+ isDisabled={isNextDisabled}
121
+ icon={<ArrowRightOutline24Icon />}
122
+ aria-label={`${t(texts.next)} ${t(texts[unit])}`}
123
+ />
124
+ </Flex>
125
+ );
126
+ };
127
+
128
+ const texts = createTexts({
129
+ previous: {
130
+ nb: "Forrige",
131
+ nn: "Forrige",
132
+ sv: "Föregående",
133
+ en: "Previous",
134
+ },
135
+ next: {
136
+ nb: "Neste",
137
+ nn: "Neste",
138
+ sv: "Nästa",
139
+ en: "Next",
140
+ },
141
+ month: {
142
+ nb: "måned",
143
+ nn: "månad",
144
+ sv: "månad",
145
+ en: "month",
146
+ },
147
+ year: {
148
+ nb: "år",
149
+ nn: "år",
150
+ sv: "år",
151
+ en: "year",
152
+ },
153
+ });
@@ -0,0 +1,26 @@
1
+ import React, { useRef } from "react";
2
+ import { AriaButtonProps, useButton } from "react-aria";
3
+ import { IconButton } from "..";
4
+
5
+ type CalendarButtonProps = AriaButtonProps<"button"> & {
6
+ icon: React.ReactElement;
7
+ "aria-label": string;
8
+ };
9
+ export function CalendarNavigationButton({
10
+ icon,
11
+ "aria-label": ariaLabel,
12
+ ...rest
13
+ }: CalendarButtonProps) {
14
+ const ref = useRef(null);
15
+ const { buttonProps } = useButton(rest, ref);
16
+ return (
17
+ <IconButton
18
+ {...buttonProps}
19
+ ref={ref}
20
+ icon={icon}
21
+ aria-label={ariaLabel}
22
+ size="sm"
23
+ variant="ghost"
24
+ />
25
+ );
26
+ }
@@ -0,0 +1,36 @@
1
+ import { Box, PopoverAnchor, useMultiStyleConfig } from "@chakra-ui/react";
2
+ import { CalendarOutline24Icon } from "@vygruppen/spor-icon-react";
3
+ import React, { useRef } from "react";
4
+ import { AriaButtonProps, useButton } from "react-aria";
5
+ import { createTexts, useTranslation } from "..";
6
+
7
+ type CalendarTriggerButtonProps = AriaButtonProps<"button">;
8
+ export const CalendarTriggerButton = (props: CalendarTriggerButtonProps) => {
9
+ const { t } = useTranslation();
10
+ const styles = useMultiStyleConfig("Datepicker", {});
11
+ const ref = useRef(null);
12
+ const { buttonProps } = useButton(props, ref);
13
+ return (
14
+ <PopoverAnchor>
15
+ <Box
16
+ ref={ref}
17
+ as="button"
18
+ type="button"
19
+ aria-label={t(texts.openCalendar)}
20
+ sx={styles.calendarTriggerButton}
21
+ {...(buttonProps as any)}
22
+ >
23
+ <CalendarOutline24Icon />
24
+ </Box>
25
+ </PopoverAnchor>
26
+ );
27
+ };
28
+
29
+ const texts = createTexts({
30
+ openCalendar: {
31
+ nb: "Åpne kalender",
32
+ nn: "Åpne kalendar",
33
+ sv: "Öppna kalender",
34
+ en: "Open calendar",
35
+ },
36
+ });
@@ -0,0 +1,51 @@
1
+ import { Box, Flex, FormLabel, useMultiStyleConfig } from "@chakra-ui/react";
2
+ import { DateValue, GregorianCalendar } from "@internationalized/date";
3
+ import { useDateFieldState } from "@react-stately/datepicker";
4
+ import { DOMAttributes, FocusableElement } from "@react-types/shared";
5
+ import React, { useRef } from "react";
6
+ import { AriaDateFieldProps, useDateField } from "react-aria";
7
+ import { DateTimeSegment } from "./DateTimeSegment";
8
+ import { useCurrentLocale } from "./utils";
9
+
10
+ function createCalendar(identifier: string) {
11
+ switch (identifier) {
12
+ case "gregory":
13
+ return new GregorianCalendar();
14
+ default:
15
+ throw new Error(`Unsupported calendar ${identifier}`);
16
+ }
17
+ }
18
+
19
+ type DateFieldProps = AriaDateFieldProps<DateValue> & {
20
+ label?: React.ReactNode;
21
+ labelProps?: DOMAttributes<FocusableElement>;
22
+ name?: string;
23
+ };
24
+ export function DateField(props: DateFieldProps) {
25
+ const locale = useCurrentLocale();
26
+ const styles = useMultiStyleConfig("Datepicker", {});
27
+ const state = useDateFieldState({
28
+ ...props,
29
+ locale,
30
+ createCalendar,
31
+ });
32
+
33
+ const ref = useRef(null);
34
+ const { fieldProps, labelProps } = useDateField(props, state, ref);
35
+
36
+ return (
37
+ <Box minWidth="6rem">
38
+ {props.label && (
39
+ <FormLabel {...props.labelProps} {...labelProps} sx={styles.inputLabel}>
40
+ {props.label}
41
+ </FormLabel>
42
+ )}
43
+ <Flex {...fieldProps} ref={ref}>
44
+ {state.segments.map((segment, i) => (
45
+ <DateTimeSegment key={i} segment={segment} state={state} />
46
+ ))}
47
+ </Flex>
48
+ <input type="hidden" value={state.value?.toString()} name={props.name} />
49
+ </Box>
50
+ );
51
+ }
@@ -0,0 +1,153 @@
1
+ import {
2
+ Box,
3
+ BoxProps,
4
+ InputGroup,
5
+ Popover,
6
+ PopoverAnchor,
7
+ PopoverArrow,
8
+ PopoverBody,
9
+ PopoverContent,
10
+ Portal,
11
+ ResponsiveValue,
12
+ useBreakpointValue,
13
+ useFormControlContext,
14
+ } from "@chakra-ui/react";
15
+ import { DateValue } from "@internationalized/date";
16
+ import { useDatePickerState } from "@react-stately/datepicker";
17
+ import { CalendarOutline24Icon } from "@vygruppen/spor-icon-react";
18
+ import React, { useRef } from "react";
19
+ import { AriaDatePickerProps, I18nProvider, useDatePicker } from "react-aria";
20
+ import { FormErrorMessage } from "..";
21
+ import { Calendar } from "./Calendar";
22
+ import { CalendarTriggerButton } from "./CalendarTriggerButton";
23
+ import { DateField } from "./DateField";
24
+ import { StyledField } from "./StyledField";
25
+ import { useCurrentLocale } from "./utils";
26
+
27
+ type DatePickerProps = AriaDatePickerProps<DateValue> &
28
+ Pick<BoxProps, "minHeight"> & {
29
+ variant: ResponsiveValue<"simple" | "with-trigger">;
30
+ name?: string;
31
+ showYearNavigation?: boolean;
32
+ };
33
+ /**
34
+ * A date picker component.
35
+ *
36
+ * There are two versions of this component – a simple one, and one with a trigger button for showing the calendar. Use whatever fits your design.
37
+ *
38
+ * ```tsx
39
+ * <DatePicker label="Dato" variant="simple" />
40
+ * ```
41
+ */
42
+ export function DatePicker({
43
+ variant,
44
+ errorMessage,
45
+ minHeight,
46
+ showYearNavigation,
47
+ ...props
48
+ }: DatePickerProps) {
49
+ const formControlProps = useFormControlContext();
50
+ const state = useDatePickerState({
51
+ ...props,
52
+ shouldCloseOnSelect: true,
53
+ errorMessage,
54
+ isRequired: props.isRequired ?? formControlProps?.isRequired,
55
+ validationState: formControlProps?.isInvalid ? "invalid" : "valid",
56
+ });
57
+ const ref = useRef(null);
58
+ const {
59
+ groupProps,
60
+ labelProps,
61
+ fieldProps,
62
+ buttonProps,
63
+ dialogProps,
64
+ calendarProps,
65
+ errorMessageProps,
66
+ } = useDatePicker(props, state, ref);
67
+
68
+ const responsiveVariant =
69
+ useBreakpointValue(typeof variant === "string" ? [variant] : variant) ??
70
+ "simple";
71
+
72
+ const locale = useCurrentLocale();
73
+
74
+ const handleEnterClick = (e: React.KeyboardEvent) => {
75
+ if (responsiveVariant === "simple" && e.key === "Enter" && !state.isOpen) {
76
+ // Don't submit the form
77
+ e.stopPropagation();
78
+ state.setOpen(true);
79
+ }
80
+ };
81
+
82
+ const onFieldClick = () => {
83
+ if (!hasTrigger) {
84
+ state.setOpen(true);
85
+ }
86
+ };
87
+
88
+ const hasTrigger = responsiveVariant === "with-trigger";
89
+
90
+ return (
91
+ <I18nProvider locale={locale}>
92
+ <Box position="relative" display="inline-flex" flexDirection="column">
93
+ <Popover
94
+ {...dialogProps}
95
+ isOpen={state.isOpen}
96
+ onClose={() => state.setOpen(false)}
97
+ onOpen={() => state.setOpen(true)}
98
+ closeOnBlur
99
+ closeOnEsc
100
+ returnFocusOnClose
101
+ >
102
+ <InputGroup
103
+ {...groupProps}
104
+ ref={ref}
105
+ width="auto"
106
+ display="inline-flex"
107
+ >
108
+ <PopoverAnchor>
109
+ <StyledField
110
+ variant={responsiveVariant}
111
+ onClick={onFieldClick}
112
+ onKeyPress={handleEnterClick}
113
+ paddingX={3}
114
+ minHeight={minHeight}
115
+ >
116
+ {!hasTrigger && (
117
+ <CalendarOutline24Icon marginRight={2} alignSelf="center" />
118
+ )}
119
+ <DateField
120
+ label={props.label}
121
+ labelProps={labelProps}
122
+ name={props.name}
123
+ {...fieldProps}
124
+ />
125
+ </StyledField>
126
+ </PopoverAnchor>
127
+ {hasTrigger && <CalendarTriggerButton {...buttonProps} />}
128
+ </InputGroup>
129
+ <FormErrorMessage {...errorMessageProps}>
130
+ {errorMessage}
131
+ </FormErrorMessage>
132
+ {state.isOpen && !props.isDisabled && (
133
+ <Portal>
134
+ <PopoverContent
135
+ backgroundColor="white"
136
+ color="darkGrey"
137
+ boxShadow="md"
138
+ >
139
+ <PopoverArrow backgroundColor="white" />
140
+ <PopoverBody>
141
+ <Calendar
142
+ {...calendarProps}
143
+ showYearNavigation={showYearNavigation}
144
+ />
145
+ </PopoverBody>
146
+ </PopoverContent>
147
+ </Portal>
148
+ )}
149
+ </Popover>
150
+ </Box>
151
+ </I18nProvider>
152
+ );
153
+ }