@vygruppen/spor-react 12.13.2 → 12.13.4

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@vygruppen/spor-react",
3
3
  "type": "module",
4
- "version": "12.13.2",
4
+ "version": "12.13.4",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -46,8 +46,8 @@
46
46
  "react-stately": "^3.31.1",
47
47
  "react-swipeable": "^7.0.1",
48
48
  "usehooks-ts": "^3.1.0",
49
- "@vygruppen/spor-icon-react": "4.2.1",
50
49
  "@vygruppen/spor-design-tokens": "4.1.0",
50
+ "@vygruppen/spor-icon-react": "4.2.1",
51
51
  "@vygruppen/spor-loader": "0.7.0"
52
52
  },
53
53
  "devDependencies": {
@@ -10,6 +10,7 @@ import {
10
10
  useFieldContext,
11
11
  useSlotRecipe,
12
12
  } from "@chakra-ui/react";
13
+ import { CalendarOutline24Icon } from "@vygruppen/spor-icon-react";
13
14
  import React, { forwardRef, PropsWithChildren, useId, useRef } from "react";
14
15
  import {
15
16
  AriaDatePickerProps,
@@ -42,6 +43,10 @@ type DatePickerProps = Omit<AriaDatePickerProps<DateValue>, "onChange"> &
42
43
  withPortal?: boolean;
43
44
  onChange?: (value: DateValue | null) => void;
44
45
  positioning?: PopoverRootProps["positioning"];
46
+ noCalendar?: boolean;
47
+ overrideBorderColor?: string;
48
+ isActive?: boolean;
49
+ onClick?: () => void;
45
50
  } & FieldBaseProps;
46
51
 
47
52
  /**
@@ -97,8 +102,14 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
97
102
  const styles = recipe({ variant });
98
103
  const locale = useCurrentLocale();
99
104
 
105
+ const shouldShowCalendar =
106
+ state.isOpen && !props.isDisabled && !props.noCalendar;
107
+
100
108
  const onFieldClick = () => {
101
- state.setOpen(true);
109
+ if (!props.noCalendar) {
110
+ state.setOpen(true);
111
+ }
112
+ props.onClick?.();
102
113
  };
103
114
 
104
115
  const popoverContent = (
@@ -139,16 +150,25 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
139
150
  paddingX={3}
140
151
  minHeight={minHeight}
141
152
  isDisabled={props.isDisabled}
153
+ isActive={props.isActive}
154
+ overrideBorderColor={props.overrideBorderColor}
142
155
  >
143
- <ChakraPopover.Trigger asChild>
144
- <CalendarTriggerButton
145
- paddingLeft={1}
146
- paddingRight={1}
147
- variant={variant}
148
- ref={ref}
149
- {...buttonProps}
150
- />
151
- </ChakraPopover.Trigger>
156
+ {props.noCalendar ? (
157
+ <Box pr={3} pl={0.5} mr={0.5}>
158
+ <CalendarOutline24Icon />
159
+ </Box>
160
+ ) : (
161
+ <ChakraPopover.Trigger asChild>
162
+ <CalendarTriggerButton
163
+ paddingLeft={1}
164
+ paddingRight={1}
165
+ variant={variant}
166
+ ref={ref}
167
+ {...buttonProps}
168
+ />
169
+ </ChakraPopover.Trigger>
170
+ )}
171
+
152
172
  <DateField
153
173
  label={props.label}
154
174
  labelProps={labelProps}
@@ -159,10 +179,8 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
159
179
  </PopoverAnchor>
160
180
  </Field>
161
181
 
162
- {state.isOpen && !props.isDisabled && withPortal && (
163
- <Portal>{popoverContent}</Portal>
164
- )}
165
- {state.isOpen && !props.isDisabled && !withPortal && popoverContent}
182
+ {shouldShowCalendar &&
183
+ (withPortal ? <Portal>{popoverContent}</Portal> : popoverContent)}
166
184
  </ChakraPopover.Root>
167
185
  </Box>
168
186
  </I18nProvider>
@@ -14,10 +14,19 @@ type StyledFieldProps = BoxProps &
14
14
  PropsWithChildren<DatePickerVariantProps> &
15
15
  CalendarVariants & {
16
16
  isDisabled?: boolean;
17
+ isActive?: boolean;
18
+ overrideBorderColor?: string;
17
19
  };
18
20
  export const StyledField = forwardRef<HTMLDivElement, StyledFieldProps>(
19
21
  function StyledField(props, ref) {
20
- const { children, variant, isDisabled, ...otherProps } = props;
22
+ const {
23
+ children,
24
+ variant,
25
+ isDisabled,
26
+ isActive,
27
+ overrideBorderColor,
28
+ ...otherProps
29
+ } = props;
21
30
  const { invalid } = useFieldContext() ?? {
22
31
  isInvalid: false,
23
32
  };
@@ -30,7 +39,11 @@ export const StyledField = forwardRef<HTMLDivElement, StyledFieldProps>(
30
39
  return (
31
40
  <Box
32
41
  {...otherProps}
33
- css={styles.wrapper}
42
+ css={{
43
+ ...styles.wrapper,
44
+ outlineColor: overrideBorderColor || undefined,
45
+ }}
46
+ data-active={isActive ? "" : undefined}
34
47
  ref={ref}
35
48
  aria-invalid={invalid}
36
49
  aria-disabled={isDisabled}
@@ -26,6 +26,7 @@ export type FieldBaseProps = {
26
26
  helperText?: React.ReactNode;
27
27
  errorText?: React.ReactNode;
28
28
  floatingLabel?: boolean;
29
+ shouldFloat?: boolean;
29
30
  };
30
31
 
31
32
  export type FieldProps = Omit<
@@ -66,6 +67,7 @@ export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
66
67
  required,
67
68
  direction,
68
69
  id,
70
+ shouldFloat,
69
71
  ...rest
70
72
  } = props;
71
73
  const recipe = useSlotRecipe({ key: "field" });
@@ -92,7 +94,7 @@ export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
92
94
  {children}
93
95
 
94
96
  {label && floatingLabel && (
95
- <FloatingLabel>
97
+ <FloatingLabel data-float={shouldFloat ? true : undefined}>
96
98
  {label}
97
99
  <ChakraField.RequiredIndicator />
98
100
  </FloatingLabel>
@@ -12,8 +12,6 @@ FloatingLabel.displayName = "FloatingLabel";
12
12
  const floatingLabelStyles = defineStyle({
13
13
  paddingX: 3,
14
14
  fontWeight: "normal",
15
- fontSize: ["mobile.xs", "desktop.xs"],
16
- color: "text",
17
15
  pointerEvents: "none",
18
16
  zIndex: "docked",
19
17
  _disabled: {
@@ -21,16 +19,13 @@ const floatingLabelStyles = defineStyle({
21
19
  },
22
20
 
23
21
  pos: "absolute",
24
- top: "0.3rem",
25
22
  transition: "position",
26
- _peerPlaceholderShown: {
27
- /* For when input is not in focus */
28
- top: "0.9rem",
29
- color: "text",
30
- fontSize: ["mobile.sm", "desktop.sm"],
31
- },
32
- _peerFocusVisible: {
33
- /* For when input is in focus */
23
+
24
+ top: "0.9rem",
25
+ color: "text",
26
+ fontSize: ["mobile.sm", "desktop.sm"],
27
+
28
+ "&[data-float]": {
34
29
  fontSize: ["mobile.xs", "desktop.xs"],
35
30
  color: "text",
36
31
  top: "0.3rem",
@@ -7,7 +7,7 @@ import {
7
7
  InputElement,
8
8
  useRecipe,
9
9
  } from "@chakra-ui/react";
10
- import React, { ComponentProps, forwardRef, ReactNode } from "react";
10
+ import React, { ComponentProps, forwardRef, ReactNode, useState } from "react";
11
11
 
12
12
  type ChakraInputProps = ComponentProps<typeof ChakraInput>;
13
13
 
@@ -74,6 +74,20 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
74
74
  const [recipeProps, restProps] = recipe.splitVariantProps(props);
75
75
  const styles = recipe(recipeProps);
76
76
 
77
+ const [focused, setFocused] = useState(false);
78
+
79
+ const isControlled = props.value !== undefined;
80
+
81
+ const [uncontrolledValue, setUncontrolledValue] = useState(
82
+ props.defaultValue ? String(props.defaultValue) : "",
83
+ );
84
+
85
+ const inputValue = isControlled
86
+ ? String(props.value ?? "")
87
+ : uncontrolledValue;
88
+
89
+ const shouldFloat = inputValue.length > 0 || focused;
90
+
77
91
  return (
78
92
  <Field
79
93
  invalid={invalid}
@@ -83,13 +97,13 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
83
97
  errorText={errorText}
84
98
  id={props.id}
85
99
  label={
86
- // Render startElement invisibly to align label text with input content when an icon is present
87
100
  <Flex>
88
101
  <Box visibility="hidden">{startElement}</Box>
89
102
  {label}
90
103
  </Flex>
91
104
  }
92
105
  floatingLabel={true}
106
+ shouldFloat={shouldFloat}
93
107
  >
94
108
  {startElement && (
95
109
  <InputElement pointerEvents="none" paddingX={2}>
@@ -105,9 +119,25 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
105
119
  paddingRight={endElement ? "2.6rem" : undefined}
106
120
  {...restProps}
107
121
  className={`peer ${props.className}`}
122
+ value={isControlled ? props.value : undefined}
123
+ onFocus={(e) => {
124
+ props.onFocus?.(e);
125
+ setFocused(true);
126
+ }}
127
+ onBlur={(e) => {
128
+ props.onBlur?.(e);
129
+ setFocused(false);
130
+ }}
131
+ onChange={(e) => {
132
+ props.onChange?.(e);
133
+ if (!isControlled) {
134
+ setUncontrolledValue(e.target.value);
135
+ }
136
+ }}
108
137
  placeholder=""
109
138
  css={styles}
110
139
  />
140
+
111
141
  {endElement && (
112
142
  <InputElement placement="end" paddingX={2}>
113
143
  {endElement}
@@ -117,4 +147,5 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
117
147
  );
118
148
  },
119
149
  );
150
+
120
151
  Input.displayName = "Input";
@@ -68,6 +68,7 @@ export const NativeSelect = React.forwardRef<
68
68
  errorText={errorText}
69
69
  id={rest.id}
70
70
  floatingLabel={true}
71
+ shouldFloat={true}
71
72
  >
72
73
  <ChakraNativeSelect.Root
73
74
  ref={ref}
@@ -85,6 +85,11 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
85
85
  readOnly,
86
86
  helperText,
87
87
  floatingLabel,
88
+ value,
89
+ defaultValue,
90
+ onFocus,
91
+ onBlur,
92
+ onChange,
88
93
  ...restProps
89
94
  } = props;
90
95
  const recipe = useRecipe({ key: "textarea" });
@@ -92,6 +97,14 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
92
97
 
93
98
  const { labelRef, labelHeight } = useLabelHeight(label);
94
99
 
100
+ const [focused, setFocused] = useState(false);
101
+ const isControlled = value !== undefined;
102
+ const [uncontrolledValue, setUncontrolledValue] = useState(
103
+ defaultValue ? String(defaultValue) : "",
104
+ );
105
+ const inputValue = isControlled ? String(value ?? "") : uncontrolledValue;
106
+ const shouldFloat = inputValue.length > 0 || focused;
107
+
95
108
  return (
96
109
  <Field
97
110
  errorText={errorText}
@@ -100,6 +113,7 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
100
113
  required={required}
101
114
  readOnly={readOnly}
102
115
  floatingLabel={floatingLabel}
116
+ shouldFloat={shouldFloat}
103
117
  position="relative"
104
118
  >
105
119
  <ChakraTextarea
@@ -107,14 +121,34 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
107
121
  css={styles}
108
122
  className="peer"
109
123
  ref={ref}
124
+ value={isControlled ? value : undefined}
125
+ defaultValue={defaultValue}
126
+ onFocus={(e) => {
127
+ onFocus?.(e);
128
+ setFocused(true);
129
+ }}
130
+ onBlur={(e) => {
131
+ onBlur?.(e);
132
+ setFocused(false);
133
+ }}
134
+ onChange={(e) => {
135
+ onChange?.(e);
136
+ if (!isControlled) setUncontrolledValue(e.target.value);
137
+ }}
110
138
  style={
111
139
  { "--label-height": `${labelHeight}px` } as React.CSSProperties
112
140
  }
113
141
  placeholder=" "
114
142
  />
115
- <FloatingLabel ref={labelRef}>{label}</FloatingLabel>
143
+ <FloatingLabel
144
+ ref={labelRef}
145
+ data-float={shouldFloat ? true : undefined}
146
+ >
147
+ {label}
148
+ </FloatingLabel>
116
149
  </Field>
117
150
  );
118
151
  },
119
152
  );
153
+
120
154
  Textarea.displayName = "Textarea";
@@ -28,6 +28,13 @@ export const datePickerSlotRecipe = defineSlotRecipe({
28
28
  outline: "2px solid",
29
29
  outlineColor: "outline.focus",
30
30
  },
31
+ "&[data-active]": {
32
+ outline: "2px solid",
33
+ outlineColor: "outline.focus",
34
+ "&:hover": {
35
+ outlineColor: "outline.focus",
36
+ },
37
+ },
31
38
  },
32
39
  inputLabel: {
33
40
  fontSize: "mobile.xs",
@@ -158,6 +165,14 @@ export const datePickerSlotRecipe = defineSlotRecipe({
158
165
  outline: "1px solid",
159
166
  outlineColor: "core.outline",
160
167
  },
168
+
169
+ "&[data-active]": {
170
+ outline: "2px solid",
171
+ outlineColor: "outline.focus",
172
+ _hover: {
173
+ outlineColor: "outline.focus",
174
+ },
175
+ },
161
176
  },
162
177
  _invalid: {
163
178
  outline: "2px solid",
@@ -183,6 +198,14 @@ export const datePickerSlotRecipe = defineSlotRecipe({
183
198
  outline: "1px solid",
184
199
  outlineColor: "core.outline",
185
200
  },
201
+
202
+ "&[data-active]": {
203
+ outline: "2px solid",
204
+ outlineColor: "outline.focus",
205
+ _hover: {
206
+ outlineColor: "outline.focus",
207
+ },
208
+ },
186
209
  },
187
210
  _invalid: {
188
211
  outline: "2px solid",
@@ -200,6 +223,14 @@ export const datePickerSlotRecipe = defineSlotRecipe({
200
223
  outline: "1px solid",
201
224
  outlineColor: "core.outline",
202
225
  },
226
+
227
+ "&[data-active]": {
228
+ outline: "2px solid",
229
+ outlineColor: "outline.focus",
230
+ _hover: {
231
+ outlineColor: "outline.focus",
232
+ },
233
+ },
203
234
  },
204
235
  _invalid: {
205
236
  outline: "2px solid",