@vygruppen/spor-react 1.3.3 → 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.
- package/.turbo/turbo-build.log +12 -10
- package/CHANGELOG.md +40 -0
- package/README.md +1 -1
- package/dist/index.d.ts +6718 -27
- package/dist/index.js +14139 -140
- package/dist/index.mjs +13818 -27
- package/package.json +19 -31
- package/src/accordion/Accordion.test.tsx +20 -0
- package/src/accordion/Accordion.tsx +62 -0
- package/src/accordion/AccordionContext.tsx +27 -0
- package/src/accordion/Expandable.tsx +157 -0
- package/src/accordion/index.tsx +2 -0
- package/src/alert/AlertIcon.tsx +75 -0
- package/src/alert/BaseAlert.test.tsx +37 -0
- package/src/alert/BaseAlert.tsx +21 -0
- package/src/alert/ClosableAlert.test.tsx +37 -0
- package/src/alert/ClosableAlert.tsx +75 -0
- package/src/alert/ExpandableAlert.test.tsx +84 -0
- package/src/alert/ExpandableAlert.tsx +84 -0
- package/src/alert/StaticAlert.tsx +25 -0
- package/src/alert/index.tsx +3 -0
- package/src/button/Button.test.tsx +23 -0
- package/src/button/Button.tsx +162 -0
- package/src/button/ButtonGroup.tsx +43 -0
- package/src/button/CloseButton.tsx +63 -0
- package/src/button/FloatingActionButton.tsx +113 -0
- package/src/button/IconButton.tsx +63 -0
- package/src/button/index.tsx +5 -0
- package/src/card/Card.tsx +59 -0
- package/src/card/index.tsx +1 -0
- package/src/datepicker/Calendar.tsx +32 -0
- package/src/datepicker/CalendarCell.tsx +74 -0
- package/src/datepicker/CalendarGrid.tsx +76 -0
- package/src/datepicker/CalendarHeader.tsx +153 -0
- package/src/datepicker/CalendarNavigationButton.tsx +26 -0
- package/src/datepicker/CalendarTriggerButton.tsx +36 -0
- package/src/datepicker/DateField.tsx +51 -0
- package/src/datepicker/DatePicker.tsx +153 -0
- package/src/datepicker/DateRangePicker.tsx +165 -0
- package/src/datepicker/DateTimeSegment.tsx +56 -0
- package/src/datepicker/RangeCalendar.tsx +35 -0
- package/src/datepicker/StyledField.tsx +31 -0
- package/src/datepicker/TimeField.tsx +46 -0
- package/src/datepicker/TimePicker.test.tsx +74 -0
- package/src/datepicker/TimePicker.tsx +196 -0
- package/src/datepicker/index.tsx +4 -0
- package/src/datepicker/utils.ts +33 -0
- package/src/i18n/index.tsx +38 -0
- package/src/image/index.tsx +2 -0
- package/src/index.tsx +25 -26
- package/src/input/CardSelect.tsx +165 -0
- package/src/input/Checkbox.tsx +24 -0
- package/src/input/CheckboxGroup.tsx +43 -0
- package/src/input/ChoiceChip.tsx +102 -0
- package/src/input/Dialog.tsx +29 -0
- package/src/input/FormControl.tsx +11 -0
- package/src/input/FormErrorMessage.tsx +91 -0
- package/src/input/FormLabel.tsx +11 -0
- package/src/input/InfoSelect.tsx +209 -0
- package/src/input/Input.tsx +59 -0
- package/src/input/InputElement.tsx +45 -0
- package/src/input/ListBox.tsx +123 -0
- package/src/input/NativeSelect.tsx +38 -0
- package/src/input/PasswordInput.tsx +70 -0
- package/src/input/Popover.tsx +70 -0
- package/src/input/Radio.tsx +34 -0
- package/src/input/RadioGroup.tsx +47 -0
- package/src/input/SearchInput.tsx +89 -0
- package/src/input/Switch.tsx +40 -0
- package/src/input/Textarea.tsx +98 -0
- package/src/input/index.tsx +20 -0
- package/src/layout/Divider.tsx +26 -0
- package/src/layout/Stack.tsx +42 -0
- package/src/layout/index.tsx +28 -0
- package/src/linjetag/InfoTag.tsx +54 -0
- package/src/linjetag/LineIcon.tsx +44 -0
- package/src/linjetag/TravelTag.tsx +121 -0
- package/src/linjetag/icons.tsx +80 -0
- package/src/linjetag/index.tsx +3 -0
- package/src/linjetag/types.d.ts +24 -0
- package/src/link/TextLink.tsx +45 -0
- package/src/link/index.tsx +1 -0
- package/src/loader/ClientOnly.tsx +29 -0
- package/src/loader/ColorInlineLoader.tsx +27 -0
- package/src/loader/ColorSpinner.tsx +44 -0
- package/src/loader/ContentLoader.tsx +27 -0
- package/src/loader/DarkFullScreenLoader.tsx +23 -0
- package/src/loader/DarkInlineLoader.tsx +25 -0
- package/src/loader/DarkSpinner.tsx +43 -0
- package/src/loader/LightFullScreenLoader.tsx +23 -0
- package/src/loader/LightInlineLoader.tsx +25 -0
- package/src/loader/LightSpinner.tsx +41 -0
- package/src/loader/Lottie.tsx +10 -0
- package/src/loader/ProgressBar.tsx +128 -0
- package/src/loader/ProgressLoader.tsx +140 -0
- package/src/loader/Skeleton.tsx +16 -0
- package/src/loader/SkeletonCircle.tsx +13 -0
- package/src/loader/SkeletonText.tsx +10 -0
- package/src/loader/index.tsx +14 -0
- package/src/loader/useHydrated.tsx +34 -0
- package/src/loader/useRotatingLabel.tsx +22 -0
- package/src/logo/VyLogo.tsx +101 -0
- package/src/logo/index.tsx +1 -0
- package/src/media-controller/JumpButton.tsx +69 -0
- package/src/media-controller/PlayPauseButton.tsx +67 -0
- package/src/media-controller/SkipButton.tsx +66 -0
- package/src/media-controller/icons.tsx +80 -0
- package/src/media-controller/index.test.tsx +59 -0
- package/src/media-controller/index.tsx +3 -0
- package/src/modal/Drawer.tsx +122 -0
- package/src/modal/Modal.tsx +15 -0
- package/src/modal/ModalHeader.tsx +31 -0
- package/src/modal/SimpleDrawer.tsx +44 -0
- package/src/modal/index.tsx +4 -0
- package/src/popover/PopoverWizardBody.tsx +91 -0
- package/src/popover/SimplePopover.tsx +75 -0
- package/src/popover/WizardPopover.tsx +61 -0
- package/src/popover/index.tsx +23 -0
- package/src/provider/SporProvider.tsx +67 -0
- package/src/provider/index.tsx +1 -0
- package/src/stepper/Stepper.tsx +115 -0
- package/src/stepper/StepperContext.tsx +55 -0
- package/src/stepper/StepperStep.tsx +48 -0
- package/src/stepper/index.tsx +2 -0
- package/src/tab/Tabs.tsx +20 -0
- package/src/tab/index.tsx +9 -0
- package/src/table/Table.tsx +58 -0
- package/src/table/index.tsx +19 -0
- package/src/theme/components/accordion.ts +143 -0
- package/src/theme/components/alert.ts +59 -0
- package/src/theme/components/badge.ts +109 -0
- package/src/theme/components/button.ts +217 -0
- package/src/theme/components/card-select.ts +158 -0
- package/src/theme/components/card.ts +174 -0
- package/src/theme/components/checkbox.ts +90 -0
- package/src/theme/components/choice-chip.ts +79 -0
- package/src/theme/components/close-button.ts +56 -0
- package/src/theme/components/code.ts +17 -0
- package/src/theme/components/datepicker.ts +194 -0
- package/src/theme/components/drawer.ts +92 -0
- package/src/theme/components/fab.ts +111 -0
- package/src/theme/components/form-label.ts +17 -0
- package/src/theme/components/form.ts +27 -0
- package/src/theme/components/index.ts +34 -0
- package/src/theme/components/info-select.ts +91 -0
- package/src/theme/components/info-tag.ts +49 -0
- package/src/theme/components/input.ts +97 -0
- package/src/theme/components/line-icon.ts +121 -0
- package/src/theme/components/link.ts +155 -0
- package/src/theme/components/listbox.ts +52 -0
- package/src/theme/components/media-controller-button.ts +134 -0
- package/src/theme/components/modal.ts +93 -0
- package/src/theme/components/popover.ts +63 -0
- package/src/theme/components/radio.ts +64 -0
- package/src/theme/components/select.ts +52 -0
- package/src/theme/components/skeleton.ts +40 -0
- package/src/theme/components/stepper.ts +230 -0
- package/src/theme/components/switch.ts +227 -0
- package/src/theme/components/table.ts +163 -0
- package/src/theme/components/tabs.ts +282 -0
- package/src/theme/components/textarea.ts +14 -0
- package/src/theme/components/toast.ts +28 -0
- package/src/theme/components/travel-tag.ts +267 -0
- package/src/theme/font-faces.ts +66 -0
- package/src/theme/foundations/borders.ts +11 -0
- package/src/theme/foundations/breakpoints.ts +9 -0
- package/src/theme/foundations/colors.ts +10 -0
- package/src/theme/foundations/config.ts +5 -0
- package/src/theme/foundations/fontSizes.ts +29 -0
- package/src/theme/foundations/fontWeights.ts +5 -0
- package/src/theme/foundations/fonts.ts +7 -0
- package/src/theme/foundations/index.ts +14 -0
- package/src/theme/foundations/lineHeights.ts +5 -0
- package/src/theme/foundations/radii.ts +12 -0
- package/src/theme/foundations/shadows.ts +8 -0
- package/src/theme/foundations/sizes.ts +34 -0
- package/src/theme/foundations/spacing.ts +30 -0
- package/src/theme/foundations/textStyles.ts +60 -0
- package/src/theme/foundations/zIndices.ts +17 -0
- package/src/theme/index.ts +14 -0
- package/src/theme/utils/box-shadow-utils.ts +44 -0
- package/src/theme/utils/focus-utils.ts +16 -0
- package/src/toast/ActionToast.test.tsx +22 -0
- package/src/toast/ActionToast.tsx +28 -0
- package/src/toast/BaseToast.test.tsx +27 -0
- package/src/toast/BaseToast.tsx +75 -0
- package/src/toast/ClosableToast.test.tsx +17 -0
- package/src/toast/ClosableToast.tsx +40 -0
- package/src/toast/index.tsx +1 -0
- package/src/toast/useToast.tsx +99 -0
- package/src/typography/Badge.tsx +68 -0
- package/src/typography/Code.tsx +32 -0
- package/src/typography/Heading.tsx +32 -0
- package/src/typography/Text.tsx +26 -0
- package/src/typography/index.tsx +4 -0
- package/src/util/externals.tsx +23 -0
- package/src/util/index.tsx +1 -0
@@ -0,0 +1,165 @@
|
|
1
|
+
import {
|
2
|
+
Box,
|
3
|
+
BoxProps,
|
4
|
+
FormLabel,
|
5
|
+
InputGroup,
|
6
|
+
Popover,
|
7
|
+
PopoverAnchor,
|
8
|
+
PopoverArrow,
|
9
|
+
PopoverBody,
|
10
|
+
PopoverContent,
|
11
|
+
Portal,
|
12
|
+
ResponsiveValue,
|
13
|
+
useBreakpointValue,
|
14
|
+
useFormControlContext,
|
15
|
+
useMultiStyleConfig,
|
16
|
+
} from "@chakra-ui/react";
|
17
|
+
import { DateValue } from "@internationalized/date";
|
18
|
+
import { useDateRangePickerState } from "@react-stately/datepicker";
|
19
|
+
import { CalendarOutline24Icon } from "@vygruppen/spor-icon-react";
|
20
|
+
import React, { useRef } from "react";
|
21
|
+
import {
|
22
|
+
AriaDateRangePickerProps,
|
23
|
+
I18nProvider,
|
24
|
+
useDateRangePicker,
|
25
|
+
} from "react-aria";
|
26
|
+
import { CalendarTriggerButton } from "./CalendarTriggerButton";
|
27
|
+
import { DateField } from "./DateField";
|
28
|
+
import { RangeCalendar } from "./RangeCalendar";
|
29
|
+
import { StyledField } from "./StyledField";
|
30
|
+
import { useCurrentLocale } from "./utils";
|
31
|
+
|
32
|
+
type DateRangePickerProps = AriaDateRangePickerProps<DateValue> &
|
33
|
+
Pick<BoxProps, "minHeight"> & {
|
34
|
+
startLabel?: string;
|
35
|
+
endLabel?: string;
|
36
|
+
variant: ResponsiveValue<"simple" | "with-trigger">;
|
37
|
+
};
|
38
|
+
/**
|
39
|
+
* A date range picker component.
|
40
|
+
*
|
41
|
+
* 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.
|
42
|
+
*
|
43
|
+
* ```tsx
|
44
|
+
* <DateRangePicker startLabel="From" endLabel="To" variant="simple" />
|
45
|
+
* ```
|
46
|
+
*/
|
47
|
+
export function DateRangePicker({
|
48
|
+
variant,
|
49
|
+
minHeight,
|
50
|
+
...props
|
51
|
+
}: DateRangePickerProps) {
|
52
|
+
const formControlProps = useFormControlContext();
|
53
|
+
const state = useDateRangePickerState({
|
54
|
+
...props,
|
55
|
+
shouldCloseOnSelect: true,
|
56
|
+
isRequired: props.isRequired ?? formControlProps?.isRequired,
|
57
|
+
validationState: formControlProps.isInvalid ? "invalid" : "valid",
|
58
|
+
});
|
59
|
+
const ref = useRef(null);
|
60
|
+
const {
|
61
|
+
groupProps,
|
62
|
+
labelProps,
|
63
|
+
startFieldProps,
|
64
|
+
endFieldProps,
|
65
|
+
buttonProps,
|
66
|
+
dialogProps,
|
67
|
+
calendarProps,
|
68
|
+
} = useDateRangePicker(props, state, ref);
|
69
|
+
|
70
|
+
const responsiveVariant =
|
71
|
+
useBreakpointValue(typeof variant === "string" ? [variant] : variant) ??
|
72
|
+
"simple";
|
73
|
+
|
74
|
+
const styles = useMultiStyleConfig("Datepicker", {
|
75
|
+
variant: responsiveVariant,
|
76
|
+
});
|
77
|
+
const locale = useCurrentLocale();
|
78
|
+
|
79
|
+
const handleEnterClick = (e: React.KeyboardEvent) => {
|
80
|
+
if (e.key === "Enter" && !state.isOpen && responsiveVariant === "simple") {
|
81
|
+
// Don't submit the form
|
82
|
+
e.stopPropagation();
|
83
|
+
state.setOpen(true);
|
84
|
+
}
|
85
|
+
};
|
86
|
+
|
87
|
+
const onFieldClick = () => {
|
88
|
+
if (!hasTrigger) {
|
89
|
+
state.setOpen(true);
|
90
|
+
}
|
91
|
+
};
|
92
|
+
|
93
|
+
const hasTrigger = responsiveVariant === "with-trigger";
|
94
|
+
|
95
|
+
return (
|
96
|
+
<I18nProvider locale={locale}>
|
97
|
+
<Box position="relative" display="inline-flex" flexDirection="column">
|
98
|
+
{props.label && (
|
99
|
+
<FormLabel {...labelProps} sx={styles.inputLabel}>
|
100
|
+
{props.label}
|
101
|
+
</FormLabel>
|
102
|
+
)}
|
103
|
+
<Popover
|
104
|
+
{...dialogProps}
|
105
|
+
isOpen={state.isOpen}
|
106
|
+
onClose={() => state.setOpen(false)}
|
107
|
+
closeOnBlur
|
108
|
+
closeOnEsc
|
109
|
+
returnFocusOnClose
|
110
|
+
>
|
111
|
+
<InputGroup
|
112
|
+
{...groupProps}
|
113
|
+
ref={ref}
|
114
|
+
width="auto"
|
115
|
+
display="inline-flex"
|
116
|
+
>
|
117
|
+
<PopoverAnchor>
|
118
|
+
<StyledField
|
119
|
+
alignItems="center"
|
120
|
+
paddingX={3}
|
121
|
+
variant={responsiveVariant}
|
122
|
+
onClick={onFieldClick}
|
123
|
+
onKeyPress={handleEnterClick}
|
124
|
+
minHeight={minHeight}
|
125
|
+
>
|
126
|
+
{!hasTrigger && (
|
127
|
+
<CalendarOutline24Icon mr={2} alignSelf="center" />
|
128
|
+
)}
|
129
|
+
<DateField
|
130
|
+
{...startFieldProps}
|
131
|
+
label={props.startLabel}
|
132
|
+
labelProps={labelProps}
|
133
|
+
/>
|
134
|
+
<Box as="span" aria-hidden="true" px="2">
|
135
|
+
–
|
136
|
+
</Box>
|
137
|
+
<DateField
|
138
|
+
{...endFieldProps}
|
139
|
+
label={props.endLabel}
|
140
|
+
labelProps={labelProps}
|
141
|
+
/>
|
142
|
+
</StyledField>
|
143
|
+
</PopoverAnchor>
|
144
|
+
{hasTrigger && <CalendarTriggerButton {...buttonProps} />}
|
145
|
+
</InputGroup>
|
146
|
+
{state.isOpen && (
|
147
|
+
<Portal>
|
148
|
+
<PopoverContent
|
149
|
+
backgroundColor="white"
|
150
|
+
color="darkGrey"
|
151
|
+
boxShadow="md"
|
152
|
+
maxWidth="none"
|
153
|
+
>
|
154
|
+
<PopoverArrow backgroundColor="white" />
|
155
|
+
<PopoverBody>
|
156
|
+
<RangeCalendar {...calendarProps} />
|
157
|
+
</PopoverBody>
|
158
|
+
</PopoverContent>
|
159
|
+
</Portal>
|
160
|
+
)}
|
161
|
+
</Popover>
|
162
|
+
</Box>
|
163
|
+
</I18nProvider>
|
164
|
+
);
|
165
|
+
}
|
@@ -0,0 +1,56 @@
|
|
1
|
+
import { Box } from "@chakra-ui/react";
|
2
|
+
import React, { useRef } from "react";
|
3
|
+
import { useDateSegment } from "react-aria";
|
4
|
+
import { DateFieldState, DateSegment } from "react-stately";
|
5
|
+
|
6
|
+
type DateTimeSegmentProps = {
|
7
|
+
segment: DateSegment;
|
8
|
+
state: DateFieldState;
|
9
|
+
};
|
10
|
+
/**
|
11
|
+
* A date time segment is a part of a date or a time stamp.
|
12
|
+
*
|
13
|
+
* Examples could be the day, month, year, hour, minute, second, etc.
|
14
|
+
*
|
15
|
+
* This component should be used with the react-aria library, and is not meant to be used directly.
|
16
|
+
* */
|
17
|
+
export const DateTimeSegment = ({ segment, state }: DateTimeSegmentProps) => {
|
18
|
+
const ref = useRef(null);
|
19
|
+
|
20
|
+
const { segmentProps } = useDateSegment(segment, state, ref);
|
21
|
+
return (
|
22
|
+
<Box
|
23
|
+
{...segmentProps}
|
24
|
+
ref={ref}
|
25
|
+
style={{
|
26
|
+
...segmentProps.style,
|
27
|
+
fontVariantNumeric: "tabular-nums",
|
28
|
+
boxSizing: "content-box",
|
29
|
+
}}
|
30
|
+
paddingX="1px"
|
31
|
+
textAlign="end"
|
32
|
+
outline="none"
|
33
|
+
borderRadius="xs"
|
34
|
+
color={
|
35
|
+
segment.isPlaceholder
|
36
|
+
? "dimGrey"
|
37
|
+
: segment.isEditable
|
38
|
+
? "darkGrey"
|
39
|
+
: "osloGrey"
|
40
|
+
}
|
41
|
+
_focus={{
|
42
|
+
backgroundColor: "darkTeal",
|
43
|
+
color: "white",
|
44
|
+
}}
|
45
|
+
>
|
46
|
+
{isPaddable(segment.type) ? segment.text.padStart(2, "0") : segment.text}
|
47
|
+
</Box>
|
48
|
+
);
|
49
|
+
};
|
50
|
+
|
51
|
+
const isPaddable = (segmentType: DateSegment["type"]) =>
|
52
|
+
segmentType === "month" ||
|
53
|
+
segmentType === "day" ||
|
54
|
+
segmentType === "hour" ||
|
55
|
+
segmentType === "minute" ||
|
56
|
+
segmentType === "second";
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import { Box } from "@chakra-ui/react";
|
2
|
+
import { createCalendar, DateValue } from "@internationalized/date";
|
3
|
+
import { useRangeCalendarState } from "@react-stately/calendar";
|
4
|
+
import React, { useRef } from "react";
|
5
|
+
import {
|
6
|
+
RangeCalendarProps as ReactAriaRangeCalendarProps,
|
7
|
+
useRangeCalendar,
|
8
|
+
} from "react-aria";
|
9
|
+
import { CalendarGrid } from "./CalendarGrid";
|
10
|
+
import { CalendarHeader } from "./CalendarHeader";
|
11
|
+
import { useCurrentLocale } from "./utils";
|
12
|
+
|
13
|
+
type RangeCalendarProps = ReactAriaRangeCalendarProps<DateValue>;
|
14
|
+
export function RangeCalendar(props: RangeCalendarProps) {
|
15
|
+
const locale = useCurrentLocale();
|
16
|
+
const state = useRangeCalendarState({
|
17
|
+
...props,
|
18
|
+
visibleDuration: { months: 2 },
|
19
|
+
locale,
|
20
|
+
createCalendar,
|
21
|
+
});
|
22
|
+
|
23
|
+
const ref = useRef(null);
|
24
|
+
const { calendarProps, title } = useRangeCalendar(props, state, ref);
|
25
|
+
|
26
|
+
return (
|
27
|
+
<Box {...calendarProps} ref={ref}>
|
28
|
+
<CalendarHeader state={state} title={title} />
|
29
|
+
<Box display="flex" gap="8">
|
30
|
+
<CalendarGrid state={state} />
|
31
|
+
<CalendarGrid state={state} offset={{ months: 1 }} />
|
32
|
+
</Box>
|
33
|
+
</Box>
|
34
|
+
);
|
35
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import {
|
2
|
+
As,
|
3
|
+
Box,
|
4
|
+
BoxProps,
|
5
|
+
forwardRef,
|
6
|
+
useFormControlContext,
|
7
|
+
useMultiStyleConfig,
|
8
|
+
} from "@chakra-ui/react";
|
9
|
+
import React from "react";
|
10
|
+
|
11
|
+
type StyledFieldProps = BoxProps & {
|
12
|
+
variant: "simple" | "with-trigger";
|
13
|
+
};
|
14
|
+
export const StyledField = forwardRef<StyledFieldProps, As>(
|
15
|
+
({ children, variant, ...otherProps }, ref) => {
|
16
|
+
const { isInvalid } = useFormControlContext() ?? {
|
17
|
+
isInvalid: false,
|
18
|
+
};
|
19
|
+
const styles = useMultiStyleConfig("Datepicker", { variant });
|
20
|
+
return (
|
21
|
+
<Box
|
22
|
+
{...otherProps}
|
23
|
+
__css={styles.wrapper}
|
24
|
+
ref={ref}
|
25
|
+
aria-invalid={isInvalid}
|
26
|
+
>
|
27
|
+
{children}
|
28
|
+
</Box>
|
29
|
+
);
|
30
|
+
}
|
31
|
+
);
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import { Box, Flex } from "@chakra-ui/react";
|
2
|
+
import { CalendarDateTime, Time } from "@internationalized/date";
|
3
|
+
import React, { useRef } from "react";
|
4
|
+
import { AriaTimeFieldProps, useTimeField } from "react-aria";
|
5
|
+
import { DateFieldState } from "react-stately";
|
6
|
+
import { FormLabel } from "..";
|
7
|
+
import { DateTimeSegment } from "./DateTimeSegment";
|
8
|
+
import { getTimestampFromTime } from "./utils";
|
9
|
+
|
10
|
+
type TimeFieldProps = AriaTimeFieldProps<Time> & {
|
11
|
+
state: DateFieldState;
|
12
|
+
label: string;
|
13
|
+
name?: string;
|
14
|
+
};
|
15
|
+
/** A time field component.
|
16
|
+
*
|
17
|
+
* This component lets the user choose a time based on regular user input.
|
18
|
+
* It shouldn't be used directly, but is used by the TimePicker component.
|
19
|
+
*/
|
20
|
+
export const TimeField = ({ state, ...props }: TimeFieldProps) => {
|
21
|
+
const ref = useRef<HTMLDivElement>(null);
|
22
|
+
const { labelProps, fieldProps } = useTimeField(props, state, ref);
|
23
|
+
|
24
|
+
return (
|
25
|
+
<Box>
|
26
|
+
<FormLabel
|
27
|
+
{...labelProps}
|
28
|
+
htmlFor={fieldProps.id}
|
29
|
+
marginBottom={0}
|
30
|
+
fontSize="mobile.xs"
|
31
|
+
>
|
32
|
+
{props.label}
|
33
|
+
</FormLabel>
|
34
|
+
<Flex {...fieldProps} ref={ref}>
|
35
|
+
{state.segments.map((segment) => (
|
36
|
+
<DateTimeSegment key={segment.type} segment={segment} state={state} />
|
37
|
+
))}
|
38
|
+
</Flex>
|
39
|
+
<input
|
40
|
+
type="hidden"
|
41
|
+
value={getTimestampFromTime(state.value as CalendarDateTime | null)}
|
42
|
+
name={props.name}
|
43
|
+
/>
|
44
|
+
</Box>
|
45
|
+
);
|
46
|
+
};
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import { Time } from "@internationalized/date";
|
2
|
+
import { act, render } from "@testing-library/react";
|
3
|
+
import React from "react";
|
4
|
+
import { axe } from "vitest-axe";
|
5
|
+
import { TimePicker } from "./TimePicker";
|
6
|
+
|
7
|
+
describe("<TimePicker />", () => {
|
8
|
+
it("is accessible", async () => {
|
9
|
+
const { container } = render(<TimePicker />);
|
10
|
+
expect(await axe(container)).toHaveNoViolations();
|
11
|
+
});
|
12
|
+
|
13
|
+
it("jumps backwards as expected", async () => {
|
14
|
+
const { getByLabelText, getByRole } = render(
|
15
|
+
<TimePicker defaultValue={new Time(13, 3)} />
|
16
|
+
);
|
17
|
+
const backwardsButton = getByLabelText("Bakover 30 minutter");
|
18
|
+
expect(getByRole("group")).toHaveTextContent("13:03");
|
19
|
+
act(() => {
|
20
|
+
backwardsButton.click();
|
21
|
+
});
|
22
|
+
expect(getByRole("group")).toHaveTextContent("13:00");
|
23
|
+
act(() => {
|
24
|
+
backwardsButton.click();
|
25
|
+
});
|
26
|
+
expect(getByRole("group")).toHaveTextContent("12:30");
|
27
|
+
});
|
28
|
+
it("jumps forwards as expected", async () => {
|
29
|
+
const { getByLabelText, getByRole } = render(
|
30
|
+
<TimePicker defaultValue={new Time(13, 53)} />
|
31
|
+
);
|
32
|
+
const forwardsButton = getByLabelText("Fremover 30 minutter");
|
33
|
+
expect(getByRole("group")).toHaveTextContent("13:53");
|
34
|
+
act(() => {
|
35
|
+
forwardsButton.click();
|
36
|
+
});
|
37
|
+
expect(getByRole("group")).toHaveTextContent("14:00");
|
38
|
+
act(() => {
|
39
|
+
forwardsButton.click();
|
40
|
+
});
|
41
|
+
expect(getByRole("group")).toHaveTextContent("14:30");
|
42
|
+
});
|
43
|
+
it("jumps backwards as expected when minuteInterval is set", async () => {
|
44
|
+
const { getByLabelText, getByRole } = render(
|
45
|
+
<TimePicker defaultValue={new Time(13, 3)} minuteInterval={15} />
|
46
|
+
);
|
47
|
+
const backwardsButton = getByLabelText("Bakover 15 minutter");
|
48
|
+
expect(getByRole("group")).toHaveTextContent("13:03");
|
49
|
+
act(() => {
|
50
|
+
backwardsButton.click();
|
51
|
+
});
|
52
|
+
expect(getByRole("group")).toHaveTextContent("13:00");
|
53
|
+
act(() => {
|
54
|
+
backwardsButton.click();
|
55
|
+
});
|
56
|
+
expect(getByRole("group")).toHaveTextContent("12:45");
|
57
|
+
});
|
58
|
+
it("jumps forwards as expected when minuteInterval is set", async () => {
|
59
|
+
const { getByLabelText, getByRole } = render(
|
60
|
+
<TimePicker defaultValue={new Time(13, 49)} minuteInterval={15} />
|
61
|
+
);
|
62
|
+
|
63
|
+
const forwardsButton = getByLabelText("Fremover 15 minutter");
|
64
|
+
expect(getByRole("group")).toHaveTextContent("13:49");
|
65
|
+
act(() => {
|
66
|
+
forwardsButton.click();
|
67
|
+
});
|
68
|
+
expect(getByRole("group")).toHaveTextContent("14:00");
|
69
|
+
act(() => {
|
70
|
+
forwardsButton.click();
|
71
|
+
});
|
72
|
+
expect(getByRole("group")).toHaveTextContent("14:15");
|
73
|
+
});
|
74
|
+
});
|
@@ -0,0 +1,196 @@
|
|
1
|
+
import { BoxProps, useFormControlContext } from "@chakra-ui/react";
|
2
|
+
import { CalendarDateTime } from "@internationalized/date";
|
3
|
+
import { TimeValue } from "@react-types/datepicker";
|
4
|
+
import {
|
5
|
+
DropdownLeftFill24Icon,
|
6
|
+
DropdownRightFill24Icon,
|
7
|
+
} from "@vygruppen/spor-icon-react";
|
8
|
+
import React from "react";
|
9
|
+
import { useTimeFieldState } from "react-stately";
|
10
|
+
import { IconButton, createTexts, useTranslation } from "..";
|
11
|
+
import { StyledField } from "./StyledField";
|
12
|
+
import { TimeField } from "./TimeField";
|
13
|
+
import { getCurrentTime, useCurrentLocale } from "./utils";
|
14
|
+
|
15
|
+
type TimePickerProps = Omit<BoxProps, "defaultValue"> & {
|
16
|
+
/** The label. Defaults to a localized version of "Time" */
|
17
|
+
label?: string;
|
18
|
+
/** The name of the form field, if used in a regular form */
|
19
|
+
name?: string;
|
20
|
+
/** The controlled value, if any.
|
21
|
+
*
|
22
|
+
* A `new Time(hours, minutes)` should be passed
|
23
|
+
**/
|
24
|
+
value?: TimeValue;
|
25
|
+
/** A default value, if any.
|
26
|
+
*
|
27
|
+
* A `new Time(hours, minutes)` should be passed. Defaults to the current time if not provided.
|
28
|
+
**/
|
29
|
+
defaultValue?: TimeValue;
|
30
|
+
/** Callback for when the time changes */
|
31
|
+
onChange?: (value: TimeValue) => void;
|
32
|
+
/** The maxiumum number of minutes to move when the step buttons are used.
|
33
|
+
*
|
34
|
+
* Defaults to 30 minutes.
|
35
|
+
*
|
36
|
+
* An example: If the time is at 13:37 and the minuteInterval is 15, clicking the step forwards button will move the time to 13:45. Next click will move it to 14:00.
|
37
|
+
*/
|
38
|
+
minuteInterval?: number;
|
39
|
+
/** Whether or not the field is disabled */
|
40
|
+
isDisabled?: boolean;
|
41
|
+
};
|
42
|
+
/** A time picker component.
|
43
|
+
*
|
44
|
+
* This lets the user select a time of day, either through typing it in, using the up and down arrows to select the hour and minute, or by clicking the step buttons to move the time forwards or backwards in pre-defined increments.
|
45
|
+
*
|
46
|
+
* ```tsx
|
47
|
+
* <TimePicker />
|
48
|
+
* ```
|
49
|
+
*
|
50
|
+
* It can also be controlled:
|
51
|
+
*
|
52
|
+
* ```tsx
|
53
|
+
* <TimePicker value={new Time(13, 37)} onChange={setTime} />
|
54
|
+
* ```
|
55
|
+
*
|
56
|
+
* Note that the TimePicker uses the `Time` class to represent the time. This is a class that is part of the `@internationalized/date` package.
|
57
|
+
*
|
58
|
+
* @see https://spor.vy.no/komponenter/timepicker
|
59
|
+
*/
|
60
|
+
export const TimePicker = ({
|
61
|
+
label: externalLabel,
|
62
|
+
value,
|
63
|
+
defaultValue = getCurrentTime(),
|
64
|
+
onChange = () => {},
|
65
|
+
minuteInterval = 30,
|
66
|
+
isDisabled: isDisabledExternally = false,
|
67
|
+
name,
|
68
|
+
...boxProps
|
69
|
+
}: TimePickerProps) => {
|
70
|
+
const { isDisabled: isFormControlDisabled, isInvalid: isFormControlInvalid } =
|
71
|
+
useFormControlContext() ?? {};
|
72
|
+
const isDisabled = isDisabledExternally ?? isFormControlDisabled ?? false;
|
73
|
+
const { t } = useTranslation();
|
74
|
+
const locale = useCurrentLocale();
|
75
|
+
const label = externalLabel ?? t(texts.time);
|
76
|
+
const state = useTimeFieldState({
|
77
|
+
value,
|
78
|
+
defaultValue,
|
79
|
+
onChange,
|
80
|
+
locale,
|
81
|
+
isDisabled,
|
82
|
+
label,
|
83
|
+
validationState: isFormControlInvalid ? "invalid" : "valid",
|
84
|
+
});
|
85
|
+
|
86
|
+
const dateTime = state.value as CalendarDateTime | null;
|
87
|
+
|
88
|
+
const handleBackwardsClick = () => {
|
89
|
+
if (!dateTime) {
|
90
|
+
return;
|
91
|
+
}
|
92
|
+
const minutesToSubtract =
|
93
|
+
dateTime.minute % minuteInterval || minuteInterval;
|
94
|
+
state.setValue(
|
95
|
+
state.value.subtract({
|
96
|
+
minutes: minutesToSubtract,
|
97
|
+
})
|
98
|
+
);
|
99
|
+
};
|
100
|
+
const handleForwardClick = () => {
|
101
|
+
if (!dateTime) {
|
102
|
+
return;
|
103
|
+
}
|
104
|
+
const minutesToAdd =
|
105
|
+
minuteInterval - (dateTime.minute % minuteInterval) || minuteInterval;
|
106
|
+
state.setValue(
|
107
|
+
state.value.add({
|
108
|
+
minutes: minutesToAdd,
|
109
|
+
})
|
110
|
+
);
|
111
|
+
};
|
112
|
+
const backwardsLabel = `${t(texts.backwards)} ${minuteInterval} ${t(
|
113
|
+
texts.minutes
|
114
|
+
)}`;
|
115
|
+
const forwardsLabel = `${t(texts.forwards)} ${minuteInterval} ${t(
|
116
|
+
texts.minutes
|
117
|
+
)}`;
|
118
|
+
const inputLabel = label ?? t(texts.time);
|
119
|
+
const ariaLabel = `${inputLabel} – ${t(
|
120
|
+
texts.selectedTimeIs(`${dateTime?.hour ?? 0} ${dateTime?.minute ?? 0}`)
|
121
|
+
)}`;
|
122
|
+
return (
|
123
|
+
<StyledField
|
124
|
+
variant="simple"
|
125
|
+
width="fit-content"
|
126
|
+
paddingX={2}
|
127
|
+
paddingY={1}
|
128
|
+
alignItems="center"
|
129
|
+
justifyContent="space-between"
|
130
|
+
gap={2}
|
131
|
+
opacity={isDisabled ? 0.5 : 1}
|
132
|
+
pointerEvents={isDisabled ? "none" : "auto"}
|
133
|
+
aria-disabled={isDisabled}
|
134
|
+
aria-live="assertive"
|
135
|
+
aria-label={ariaLabel}
|
136
|
+
{...boxProps}
|
137
|
+
>
|
138
|
+
<IconButton
|
139
|
+
variant="ghost"
|
140
|
+
size="xs"
|
141
|
+
borderRadius="xs"
|
142
|
+
aria-label={backwardsLabel}
|
143
|
+
title={backwardsLabel}
|
144
|
+
icon={<DropdownLeftFill24Icon />}
|
145
|
+
onClick={handleBackwardsClick}
|
146
|
+
isDisabled={isDisabled}
|
147
|
+
style={isDisabled ? { backgroundColor: "transparent" } : {}}
|
148
|
+
/>
|
149
|
+
<TimeField label={label} state={state} name={name} />
|
150
|
+
<IconButton
|
151
|
+
variant="ghost"
|
152
|
+
size="xs"
|
153
|
+
borderRadius="xs"
|
154
|
+
aria-label={forwardsLabel}
|
155
|
+
title={forwardsLabel}
|
156
|
+
icon={<DropdownRightFill24Icon />}
|
157
|
+
onClick={handleForwardClick}
|
158
|
+
isDisabled={isDisabled}
|
159
|
+
style={isDisabled ? { backgroundColor: "transparent" } : {}}
|
160
|
+
/>
|
161
|
+
</StyledField>
|
162
|
+
);
|
163
|
+
};
|
164
|
+
|
165
|
+
const texts = createTexts({
|
166
|
+
selectedTimeIs: (time) => ({
|
167
|
+
nb: `Valgt tidspunkt er ${time}`,
|
168
|
+
nn: `Valt tidspunkt er ${time}`,
|
169
|
+
en: `Selected time is ${time}`,
|
170
|
+
sv: `Vald tid är ${time}`,
|
171
|
+
}),
|
172
|
+
time: {
|
173
|
+
nb: "Tid",
|
174
|
+
nn: "Tid",
|
175
|
+
en: "Time",
|
176
|
+
sv: "Tid",
|
177
|
+
},
|
178
|
+
backwards: {
|
179
|
+
nb: "Bakover",
|
180
|
+
nn: "Bakover",
|
181
|
+
en: "Backwards",
|
182
|
+
sv: "Bakåt",
|
183
|
+
},
|
184
|
+
forwards: {
|
185
|
+
nb: "Fremover",
|
186
|
+
nn: "Fremover",
|
187
|
+
en: "Forward",
|
188
|
+
sv: "Framåt",
|
189
|
+
},
|
190
|
+
minutes: {
|
191
|
+
nb: "minutter",
|
192
|
+
nn: "minuttar",
|
193
|
+
en: "minutes",
|
194
|
+
sv: "minuter",
|
195
|
+
},
|
196
|
+
});
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import { CalendarDateTime, parseTime } from "@internationalized/date";
|
2
|
+
import { useTranslation } from "..";
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Returns the currently selected language as a BCF47 language tag.
|
6
|
+
* This is useful for passing into the react-aria hooks
|
7
|
+
*/
|
8
|
+
export const useCurrentLocale = () => {
|
9
|
+
const { language } = useTranslation();
|
10
|
+
switch (language) {
|
11
|
+
case "nb":
|
12
|
+
return "no";
|
13
|
+
case "nn":
|
14
|
+
return "no";
|
15
|
+
case "sv":
|
16
|
+
return "sv";
|
17
|
+
case "en":
|
18
|
+
return "en-GB";
|
19
|
+
default:
|
20
|
+
return "no";
|
21
|
+
}
|
22
|
+
};
|
23
|
+
|
24
|
+
/** Gets the current time as a Time object */
|
25
|
+
export const getCurrentTime = () => {
|
26
|
+
const now = new Date();
|
27
|
+
return parseTime(now.toTimeString().split(" ")[0]);
|
28
|
+
};
|
29
|
+
|
30
|
+
/** Gets a readable timestamp from a given time object */
|
31
|
+
export const getTimestampFromTime = (time: CalendarDateTime | null) => {
|
32
|
+
return `${time?.hour ?? 0}:${time?.minute ?? 0}`;
|
33
|
+
};
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import { initLobot } from "@leile/lobo-t";
|
2
|
+
|
3
|
+
export enum Language {
|
4
|
+
NorwegianBokmal = "nb",
|
5
|
+
NorwegianNynorsk = "nn",
|
6
|
+
Swedish = "sv",
|
7
|
+
English = "en",
|
8
|
+
}
|
9
|
+
|
10
|
+
export const { LanguageProvider, useTranslation } = initLobot<typeof Language>(
|
11
|
+
Language.NorwegianBokmal
|
12
|
+
);
|
13
|
+
|
14
|
+
type LanguageObject = {
|
15
|
+
[key in Language]: string;
|
16
|
+
};
|
17
|
+
type LanguageFunction = (...args: (string | number)[]) => LanguageObject;
|
18
|
+
|
19
|
+
export type Translations = {
|
20
|
+
[key: string]: LanguageObject | LanguageFunction;
|
21
|
+
};
|
22
|
+
|
23
|
+
/** Utility function that creates type safe text objects with useTranslation
|
24
|
+
*
|
25
|
+
* ```tsx
|
26
|
+
* const texts = createTexts({
|
27
|
+
* example: {
|
28
|
+
* nb: "Eksempel",
|
29
|
+
* nn: "Døme",
|
30
|
+
* sv: "Exempel",
|
31
|
+
* en: "Example",
|
32
|
+
* }
|
33
|
+
* })
|
34
|
+
* ```
|
35
|
+
*/
|
36
|
+
export function createTexts<T extends Translations>(texts: T) {
|
37
|
+
return texts;
|
38
|
+
}
|