@vygruppen/spor-react 12.3.1 → 12.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vygruppen/spor-react",
3
- "version": "12.3.1",
3
+ "version": "12.3.2",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -56,8 +56,8 @@
56
56
  "vitest": "^0.26.3",
57
57
  "vitest-axe": "^0.1.0",
58
58
  "vitest-canvas-mock": "^0.2.2",
59
- "@vygruppen/eslint-config": "1.0.1",
60
- "@vygruppen/tsconfig": "0.1.0"
59
+ "@vygruppen/tsconfig": "0.1.0",
60
+ "@vygruppen/eslint-config": "1.0.1"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "react": ">=18.0.0 <19.0.0",
@@ -12,6 +12,7 @@ import {
12
12
  SuccessFill24Icon,
13
13
  WarningFill24Icon,
14
14
  } from "@vygruppen/spor-icon-react";
15
+ import { forwardRef } from "react";
15
16
 
16
17
  import { createTexts, useTranslation } from "../i18n";
17
18
  import { AlertProps } from "./Alert";
@@ -24,19 +25,24 @@ type AlertIconProps = {
24
25
  /**
25
26
  * Internal component that shows the correct icon for the alert
26
27
  */
27
- export const AlertIcon = ({ variant, customIcon }: AlertIconProps) => {
28
- const { t } = useTranslation();
28
+ export const AlertIcon = forwardRef<SVGSVGElement, AlertIconProps>(
29
+ ({ variant, customIcon }, ref) => {
30
+ const { t } = useTranslation();
29
31
 
30
- const icon = customIcon ?? getIcon(variant);
32
+ const Icon = customIcon ?? getIcon(variant);
31
33
 
32
- return (
33
- <Box
34
- as={icon}
35
- aria-label={t(texts[variant as keyof typeof texts])}
36
- color={customIcon ? `alert.${variant}.icon` : undefined}
37
- />
38
- );
39
- };
34
+ return (
35
+ <Box
36
+ as={Icon}
37
+ ref={ref}
38
+ aria-label={t(texts[variant as keyof typeof texts])}
39
+ color={customIcon ? `alert.${variant}.icon` : undefined}
40
+ />
41
+ );
42
+ },
43
+ );
44
+
45
+ AlertIcon.displayName = "AlertIcon";
40
46
 
41
47
  const getIcon = (variant: AlertProps["variant"]) => {
42
48
  switch (variant) {
@@ -86,7 +86,7 @@ const LoadingContent = ({
86
86
  {children}
87
87
  </Flex>
88
88
  <Center position="absolute" inset="1px 0">
89
- <ColorInlineLoader width="80%" marginX={2} marginY={2} />
89
+ <ColorInlineLoader maxWidth="8" marginX={2} marginY={2} />
90
90
  {loadingText && <Box>{loadingText}</Box>}
91
91
  </Center>
92
92
  </>
@@ -32,19 +32,19 @@ export const CloseButton = forwardRef<HTMLButtonElement, CloseButtonProps>(
32
32
  const { t } = useTranslation();
33
33
  return (
34
34
  <IconButton
35
- ref={ref}
36
35
  variant="ghost"
37
- icon={getIcon(size)}
36
+ icon={<CloseIcon size={size} />}
38
37
  size={size}
39
38
  aria-label={props["aria-label"] || t(texts.close)}
40
39
  {...props}
40
+ ref={ref}
41
41
  />
42
42
  );
43
43
  },
44
44
  );
45
45
  CloseButton.displayName = "CloseButton";
46
46
 
47
- const getIcon = (size: CloseButtonProps["size"]) => {
47
+ const CloseIcon = ({ size }: { size: CloseButtonProps["size"] }) => {
48
48
  switch (size) {
49
49
  case "xs":
50
50
  case "sm": {
@@ -56,6 +56,9 @@ const getIcon = (size: CloseButtonProps["size"]) => {
56
56
  case "lg": {
57
57
  return <CloseFill30Icon />;
58
58
  }
59
+ default: {
60
+ return <CloseFill18Icon />;
61
+ }
59
62
  }
60
63
  };
61
64
 
@@ -63,9 +63,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
63
63
  <ChakraIconButton
64
64
  aria-label={props["aria-label"]}
65
65
  size={size}
66
- ref={ref}
67
66
  position={"relative"}
68
67
  {...rest}
68
+ ref={ref}
69
69
  >
70
70
  {loading ? <ColorSpinner width="2em" height="2em" margin={1} /> : icon}
71
71
  </ChakraIconButton>
@@ -24,7 +24,7 @@ type CalendarTriggerButtonProps = AriaButtonProps<"button"> &
24
24
  export const CalendarTriggerButton = forwardRef<
25
25
  HTMLDivElement,
26
26
  CalendarTriggerButtonProps
27
- >(({ variant, disabled, ariaLabelledby, ...buttonProps }) => {
27
+ >(({ variant, disabled, ariaLabelledby, ...buttonProps }, ref) => {
28
28
  const { t } = useTranslation();
29
29
  const recipe = useSlotRecipe({
30
30
  key: "datePicker",
@@ -33,7 +33,7 @@ export const CalendarTriggerButton = forwardRef<
33
33
  const styles = recipe({ variant });
34
34
 
35
35
  return (
36
- <PopoverAnchor {...buttonProps}>
36
+ <PopoverAnchor {...buttonProps} ref={ref}>
37
37
  <IconButton
38
38
  icon={<CalendarOutline24Icon />}
39
39
  aria-label={t(texts.openCalendar)}
@@ -4,6 +4,7 @@ import {
4
4
  BoxProps,
5
5
  Popover as ChakraPopover,
6
6
  PopoverAnchor,
7
+ PopoverRootProps,
7
8
  Portal,
8
9
  RecipeVariantProps,
9
10
  useFieldContext,
@@ -40,6 +41,7 @@ type DatePickerProps = Omit<AriaDatePickerProps<DateValue>, "onChange"> &
40
41
  showYearNavigation?: boolean;
41
42
  withPortal?: boolean;
42
43
  onChange?: (value: DateValue | null) => void;
44
+ positioning?: PopoverRootProps["positioning"];
43
45
  } & FieldBaseProps;
44
46
 
45
47
  /**
@@ -63,6 +65,7 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
63
65
  width = "auto",
64
66
  invalid = false,
65
67
  helperText,
68
+ positioning,
66
69
  ...props
67
70
  },
68
71
  externalRef,
@@ -75,6 +78,7 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
75
78
  isRequired: props.isRequired ?? chakraFieldProps?.required,
76
79
  validationState: chakraFieldProps?.invalid ? "invalid" : "valid",
77
80
  });
81
+
78
82
  const internalRef = useRef<HTMLDivElement>(null);
79
83
  const ref = externalRef ?? internalRef;
80
84
  const { labelProps, fieldProps, buttonProps, dialogProps, calendarProps } =
@@ -120,7 +124,7 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
120
124
  flexDirection="column"
121
125
  width={width}
122
126
  >
123
- <ChakraPopover.Root {...dialogProps}>
127
+ <ChakraPopover.Root {...dialogProps} positioning={positioning}>
124
128
  <Field
125
129
  display="inline-flex"
126
130
  id={inputGroupId}
@@ -4,6 +4,7 @@ import {
4
4
  BoxProps,
5
5
  Popover as ChakraPopover,
6
6
  PopoverAnchor,
7
+ PopoverRootProps,
7
8
  Portal,
8
9
  useFieldContext,
9
10
  useSlotRecipe,
@@ -46,6 +47,7 @@ type DateRangePickerProps = Omit<
46
47
  end: DateValue | null;
47
48
  } | null,
48
49
  ) => void;
50
+ positioning?: PopoverRootProps["positioning"];
49
51
  } & FieldBaseProps;
50
52
  /**
51
53
  * A date range picker component.
@@ -64,6 +66,7 @@ type DateRangePickerProps = Omit<
64
66
  errorText,
65
67
  helperText,
66
68
  invalid,
69
+ positioning,
67
70
  ...props
68
71
  }: DateRangePickerProps) {
69
72
  const fieldContextPRops = useFieldContext();
@@ -118,7 +121,7 @@ type DateRangePickerProps = Omit<
118
121
  {props.label}
119
122
  </label>
120
123
  )}
121
- <ChakraPopover.Root {...dialogProps}>
124
+ <ChakraPopover.Root {...dialogProps} positioning={positioning}>
122
125
  <Field
123
126
  width="auto"
124
127
  display="inline-flex"
@@ -1,12 +1,5 @@
1
1
  "use client";
2
- import React, {
3
- forwardRef,
4
- ReactNode,
5
- useEffect,
6
- useId,
7
- useRef,
8
- useState,
9
- } from "react";
2
+ import React, { ReactNode, useEffect, useId, useRef, useState } from "react";
10
3
  import { AriaComboBoxProps, useComboBox, useFilter } from "react-aria";
11
4
  import { useComboBoxState } from "react-stately";
12
5
 
@@ -57,163 +50,162 @@ export type ComboboxProps<T> = Exclude<
57
50
  * ```
58
51
  */
59
52
 
60
- export const Combobox = forwardRef<HTMLDivElement, ComboboxProps<object>>(
61
- (props) => {
62
- const {
63
- label,
64
- loading,
65
- lefticon,
66
- righticon,
67
- borderBottomLeftRadius = "sm",
68
- borderBottomRightRadius = "sm",
69
- borderTopLeftRadius = "sm",
70
- borderTopRightRadius = "sm",
71
- marginBottom,
72
- marginTop,
73
- marginX,
74
- marginY,
75
- marginRight,
76
- marginLeft,
77
- paddingBottom,
78
- paddingRight,
79
- paddingTop,
80
- paddingLeft,
81
- paddingX,
82
- paddingY,
83
- emptyContent,
84
- inputRef: externalInputRef,
85
- children,
86
- variant,
87
- } = props;
88
- const { contains } = useFilter({ sensitivity: "base" });
53
+ export const Combobox = (props: ComboboxProps<object>) => {
54
+ const {
55
+ label,
56
+ loading,
57
+ lefticon,
58
+ righticon,
59
+ borderBottomLeftRadius = "sm",
60
+ borderBottomRightRadius = "sm",
61
+ borderTopLeftRadius = "sm",
62
+ borderTopRightRadius = "sm",
63
+ marginBottom,
64
+ marginTop,
65
+ marginX,
66
+ marginY,
67
+ marginRight,
68
+ marginLeft,
69
+ paddingBottom,
70
+ paddingRight,
71
+ paddingTop,
72
+ paddingLeft,
73
+ paddingX,
74
+ paddingY,
75
+ emptyContent,
76
+ inputRef: externalInputRef,
77
+ children,
78
+ variant,
79
+ } = props;
80
+ const { contains } = useFilter({ sensitivity: "base" });
89
81
 
90
- const fallbackInputRef = useRef<HTMLInputElement>(null);
91
- const inputRef = externalInputRef ?? fallbackInputRef;
92
- const listBoxRef = useRef<HTMLUListElement>(null);
93
- const popoverRef = useRef(null);
82
+ const fallbackInputRef = useRef<HTMLInputElement>(null);
83
+ const inputRef = externalInputRef ?? fallbackInputRef;
84
+ const listBoxRef = useRef<HTMLUListElement>(null);
85
+ const popoverRef = useRef(null);
94
86
 
95
- const listboxId = `${useId()}-listbox`;
87
+ const listboxId = `${useId()}-listbox`;
96
88
 
97
- const inputWidth = useInputWidth(inputRef);
89
+ const inputWidth = useInputWidth(inputRef);
98
90
 
99
- const state = useComboBoxState({
100
- defaultFilter: contains,
101
- shouldCloseOnBlur: true,
102
- ...props,
103
- });
91
+ const state = useComboBoxState({
92
+ defaultFilter: contains,
93
+ shouldCloseOnBlur: true,
94
+ ...props,
95
+ });
104
96
 
105
- const comboBoxProps = {
106
- borderTopLeftRadius,
107
- borderTopRightRadius,
108
- marginBottom,
109
- marginTop,
110
- marginRight,
111
- marginLeft,
112
- marginX,
113
- marginY,
114
- paddingBottom,
115
- paddingRight,
116
- paddingTop,
117
- paddingLeft,
118
- paddingX,
119
- paddingY,
120
- lefticon,
121
- };
97
+ const comboBoxProps = {
98
+ borderTopLeftRadius,
99
+ borderTopRightRadius,
100
+ marginBottom,
101
+ marginTop,
102
+ marginRight,
103
+ marginLeft,
104
+ marginX,
105
+ marginY,
106
+ paddingBottom,
107
+ paddingRight,
108
+ paddingTop,
109
+ paddingLeft,
110
+ paddingX,
111
+ paddingY,
112
+ lefticon,
113
+ };
122
114
 
123
- const {
124
- inputProps: { ...inputProps },
125
- listBoxProps,
126
- } = useComboBox(
127
- {
128
- ...props,
129
- inputRef,
130
- listBoxRef,
131
- popoverRef,
132
- label,
133
- },
134
- state,
135
- );
136
- return (
137
- <>
138
- <Input
139
- {...styleProps(comboBoxProps)}
140
- aria-haspopup="listbox"
141
- ref={inputRef}
142
- role="combobox"
143
- errorText={props.errorText}
144
- helperText={props.helperText}
145
- required={props.required}
146
- disabled={props.disabled}
147
- invalid={props.invalid}
148
- label={label}
149
- variant={variant}
150
- aria-expanded={state.isOpen}
151
- aria-autocomplete="list"
152
- aria-controls={listboxId}
153
- borderBottomLeftRadius={
154
- state.isOpen && !loading ? 0 : borderBottomLeftRadius
155
- }
156
- borderBottomRightRadius={
157
- state.isOpen && !loading ? 0 : borderBottomRightRadius
158
- }
159
- _active={{ backgroundColor: "core.surface.active" }}
160
- {...inputProps}
161
- endElement={
162
- loading ? (
163
- <ColorSpinner
164
- width="1.5rem"
165
- alignSelf="center"
166
- paddingRight={paddingRight}
167
- css={{
168
- div: {
169
- display: "flex",
170
- alignItems: "center",
171
- },
172
- }}
173
- />
174
- ) : (
175
- righticon
176
- )
177
- }
178
- placeholder=""
179
- />
180
- <span aria-hidden="true" data-trigger="multiselect"></span>
181
- {state.isOpen && !loading && (
182
- <Popover
115
+ const {
116
+ inputProps: { ...inputProps },
117
+ listBoxProps,
118
+ } = useComboBox(
119
+ {
120
+ ...props,
121
+ inputRef,
122
+ listBoxRef,
123
+ popoverRef,
124
+ label,
125
+ },
126
+ state,
127
+ );
128
+ return (
129
+ <>
130
+ <Input
131
+ {...styleProps(comboBoxProps)}
132
+ aria-haspopup="listbox"
133
+ ref={inputRef}
134
+ role="combobox"
135
+ errorText={props.errorText}
136
+ helperText={props.helperText}
137
+ required={props.required}
138
+ disabled={props.disabled}
139
+ invalid={props.invalid}
140
+ label={label}
141
+ variant={variant}
142
+ aria-expanded={state.isOpen}
143
+ aria-autocomplete="list"
144
+ aria-controls={listboxId}
145
+ borderBottomLeftRadius={
146
+ state.isOpen && !loading ? 0 : borderBottomLeftRadius
147
+ }
148
+ borderBottomRightRadius={
149
+ state.isOpen && !loading ? 0 : borderBottomRightRadius
150
+ }
151
+ _active={{ backgroundColor: "core.surface.active" }}
152
+ {...inputProps}
153
+ endElement={
154
+ loading ? (
155
+ <ColorSpinner
156
+ width="1.5rem"
157
+ alignSelf="center"
158
+ paddingRight={paddingRight}
159
+ css={{
160
+ div: {
161
+ display: "flex",
162
+ alignItems: "center",
163
+ },
164
+ }}
165
+ />
166
+ ) : (
167
+ righticon
168
+ )
169
+ }
170
+ placeholder=""
171
+ />
172
+ <span aria-hidden="true" data-trigger="multiselect"></span>
173
+ {state.isOpen && !loading && (
174
+ <Popover
175
+ state={state}
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ triggerRef={inputRef as any} /* Find a way to not use type any */
178
+ ref={popoverRef}
179
+ isNonModal
180
+ placement="bottom start"
181
+ shouldFlip={false}
182
+ hasBackdrop={false}
183
+ // The minimum padding should be 0, because the popover always should be
184
+ // aligned with the input field regardless of the left padding in the container.
185
+ containerPadding={0}
186
+ >
187
+ <ListBox
188
+ {...{
189
+ autoFocus:
190
+ typeof listBoxProps.autoFocus === "boolean"
191
+ ? listBoxProps.autoFocus
192
+ : undefined,
193
+ }}
183
194
  state={state}
184
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
185
- triggerRef={inputRef as any} /* Find a way to not use type any */
186
- ref={popoverRef}
187
- isNonModal
188
- placement="bottom start"
189
- shouldFlip={false}
190
- hasBackdrop={false}
191
- // The minimum padding should be 0, because the popover always should be
192
- // aligned with the input field regardless of the left padding in the container.
193
- containerPadding={0}
195
+ id={listboxId}
196
+ listBoxRef={listBoxRef}
197
+ emptyContent={emptyContent}
198
+ maxWidth={inputWidth}
199
+ variant={variant}
194
200
  >
195
- <ListBox
196
- {...{
197
- autoFocus:
198
- typeof listBoxProps.autoFocus === "boolean"
199
- ? listBoxProps.autoFocus
200
- : undefined,
201
- }}
202
- state={state}
203
- id={listboxId}
204
- listBoxRef={listBoxRef}
205
- emptyContent={emptyContent}
206
- maxWidth={inputWidth}
207
- variant={variant}
208
- >
209
- {children}
210
- </ListBox>
211
- </Popover>
212
- )}
213
- </>
214
- );
215
- },
216
- );
201
+ {children}
202
+ </ListBox>
203
+ </Popover>
204
+ )}
205
+ </>
206
+ );
207
+ };
208
+
217
209
  Combobox.displayName = "Combobox";
218
210
 
219
211
  const useInputWidth = (inputRef: React.RefObject<HTMLInputElement>) => {
@@ -8,13 +8,7 @@ import {
8
8
  useSlotRecipe,
9
9
  } from "@chakra-ui/react";
10
10
  import type { Node } from "@react-types/shared";
11
- import React, {
12
- forwardRef,
13
- PropsWithChildren,
14
- useContext,
15
- useEffect,
16
- useRef,
17
- } from "react";
11
+ import React, { PropsWithChildren, useContext, useEffect, useRef } from "react";
18
12
  import {
19
13
  AriaListBoxProps,
20
14
  useListBox,
@@ -84,33 +78,32 @@ type ListBoxProps<T> = AriaListBoxProps<T> &
84
78
  * ```
85
79
  */
86
80
 
87
- export const ListBox = forwardRef<HTMLDivElement, ListBoxProps<object>>(
88
- (props) => {
89
- const { loading, listBoxRef, state, maxWidth, variant, children } = props;
90
- const { listBoxProps } = useListBox(props, state, listBoxRef);
91
- const recipe = useSlotRecipe({ key: "listBox" });
92
- const styles = recipe({ variant });
93
- return (
94
- <List
95
- {...listBoxProps}
96
- ref={listBoxRef}
97
- css={styles.root}
98
- aria-busy={loading}
99
- maxWidth={maxWidth}
100
- >
101
- {state.collection.size === 0 && props.emptyContent}
102
- {[...state.collection].map((item) =>
103
- item.type === "section" ? (
104
- <ListBoxSection key={item.key} section={item} state={state} />
105
- ) : (
106
- <Option key={item.key} item={item} state={state} />
107
- ),
108
- )}
109
- {children}
110
- </List>
111
- );
112
- },
113
- );
81
+ export const ListBox = (props: ListBoxProps<object>) => {
82
+ const { loading, listBoxRef, state, maxWidth, variant, children } = props;
83
+ const { listBoxProps } = useListBox(props, state, listBoxRef);
84
+ const recipe = useSlotRecipe({ key: "listBox" });
85
+ const styles = recipe({ variant });
86
+ return (
87
+ <List
88
+ {...listBoxProps}
89
+ ref={listBoxRef}
90
+ css={styles.root}
91
+ aria-busy={loading}
92
+ maxWidth={maxWidth}
93
+ >
94
+ {state.collection.size === 0 && props.emptyContent}
95
+ {[...state.collection].map((item) =>
96
+ item.type === "section" ? (
97
+ <ListBoxSection key={item.key} section={item} state={state} />
98
+ ) : (
99
+ <Option key={item.key} item={item} state={state} />
100
+ ),
101
+ )}
102
+ {children}
103
+ </List>
104
+ );
105
+ };
106
+
114
107
  ListBox.displayName = "ListBox";
115
108
 
116
109
  /**
@@ -69,7 +69,7 @@ export type NumericStepperProps = BoxProps &
69
69
  export const NumericStepper = React.forwardRef<
70
70
  HTMLDivElement,
71
71
  NumericStepperProps
72
- >((props: NumericStepperProps) => {
72
+ >((props: NumericStepperProps, ref) => {
73
73
  const {
74
74
  name: nameProp,
75
75
  id: idProp,
@@ -102,7 +102,7 @@ export const NumericStepper = React.forwardRef<
102
102
  };
103
103
 
104
104
  return (
105
- <Field css={styles.root} width="auto" {...rest}>
105
+ <Field css={styles.root} width="auto" {...rest} ref={ref}>
106
106
  <VerySmallButton
107
107
  icon={<SubtractIcon stepLabel={clampedStepSize} />}
108
108
  aria-label={t(