@vygruppen/spor-react 12.13.2 → 12.13.3

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.3",
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,9 @@ 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;
45
49
  } & FieldBaseProps;
46
50
 
47
51
  /**
@@ -97,7 +101,11 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
97
101
  const styles = recipe({ variant });
98
102
  const locale = useCurrentLocale();
99
103
 
104
+ const shouldShowCalendar =
105
+ state.isOpen && !props.isDisabled && !props.noCalendar;
106
+
100
107
  const onFieldClick = () => {
108
+ if (props.noCalendar) return;
101
109
  state.setOpen(true);
102
110
  };
103
111
 
@@ -135,20 +143,29 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
135
143
  <PopoverAnchor>
136
144
  <StyledField
137
145
  variant={variant}
138
- onClick={onFieldClick}
146
+ onClick={props.noCalendar ? undefined : onFieldClick}
139
147
  paddingX={3}
140
148
  minHeight={minHeight}
141
149
  isDisabled={props.isDisabled}
150
+ isActive={props.isActive}
151
+ overrideBorderColor={props.overrideBorderColor}
142
152
  >
143
- <ChakraPopover.Trigger asChild>
144
- <CalendarTriggerButton
145
- paddingLeft={1}
146
- paddingRight={1}
147
- variant={variant}
148
- ref={ref}
149
- {...buttonProps}
150
- />
151
- </ChakraPopover.Trigger>
153
+ {props.noCalendar ? (
154
+ <Box pr={3} pl={0.5} mr={0.5}>
155
+ <CalendarOutline24Icon />
156
+ </Box>
157
+ ) : (
158
+ <ChakraPopover.Trigger asChild>
159
+ <CalendarTriggerButton
160
+ paddingLeft={1}
161
+ paddingRight={1}
162
+ variant={variant}
163
+ ref={ref}
164
+ {...buttonProps}
165
+ />
166
+ </ChakraPopover.Trigger>
167
+ )}
168
+
152
169
  <DateField
153
170
  label={props.label}
154
171
  labelProps={labelProps}
@@ -159,10 +176,8 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
159
176
  </PopoverAnchor>
160
177
  </Field>
161
178
 
162
- {state.isOpen && !props.isDisabled && withPortal && (
163
- <Portal>{popoverContent}</Portal>
164
- )}
165
- {state.isOpen && !props.isDisabled && !withPortal && popoverContent}
179
+ {shouldShowCalendar &&
180
+ (withPortal ? <Portal>{popoverContent}</Portal> : popoverContent)}
166
181
  </ChakraPopover.Root>
167
182
  </Box>
168
183
  </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
  };
@@ -31,6 +40,12 @@ export const StyledField = forwardRef<HTMLDivElement, StyledFieldProps>(
31
40
  <Box
32
41
  {...otherProps}
33
42
  css={styles.wrapper}
43
+ data-active={isActive ? "" : undefined}
44
+ style={
45
+ overrideBorderColor
46
+ ? { outlineColor: overrideBorderColor }
47
+ : undefined
48
+ }
34
49
  ref={ref}
35
50
  aria-invalid={invalid}
36
51
  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,10 @@ 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
+ },
31
35
  },
32
36
  inputLabel: {
33
37
  fontSize: "mobile.xs",