@vygruppen/spor-react 12.2.1 → 12.3.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.
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  chakra,
4
+ Input,
4
5
  RecipeVariantProps,
5
6
  useControllableState,
6
7
  useSlotRecipe,
@@ -9,11 +10,12 @@ import React, { PropsWithChildren, useRef } from "react";
9
10
 
10
11
  import { BoxProps, createTexts, IconButton, useTranslation } from "..";
11
12
  import { numericStepperRecipe } from "../theme/slot-recipes/numeric-stepper";
12
- import { Field } from "./Field";
13
+ import { Field, FieldBaseProps } from "./Field";
13
14
 
14
15
  type NumericStepperVariants = RecipeVariantProps<typeof numericStepperRecipe>;
15
16
 
16
17
  export type NumericStepperProps = BoxProps &
18
+ FieldBaseProps &
17
19
  PropsWithChildren<NumericStepperVariants> & {
18
20
  children: React.ReactNode;
19
21
  /** The name of the input field */
@@ -67,8 +69,8 @@ export type NumericStepperProps = BoxProps &
67
69
  export const NumericStepper = React.forwardRef<
68
70
  HTMLDivElement,
69
71
  NumericStepperProps
70
- >(
71
- ({
72
+ >((props: NumericStepperProps) => {
73
+ const {
72
74
  name: nameProp,
73
75
  id: idProp,
74
76
  value: valueProp,
@@ -81,109 +83,110 @@ export const NumericStepper = React.forwardRef<
81
83
  stepSize = 1,
82
84
  showZero = false,
83
85
  ariaLabelContext = { singular: "", plural: "" },
84
- }: NumericStepperProps) => {
85
- const addButtonRef = useRef<HTMLButtonElement>(null);
86
- const { t } = useTranslation();
87
- const recipe = useSlotRecipe({ recipe: numericStepperRecipe });
88
- const styles = recipe();
89
- const [value, onChange] = useControllableState<number>({
90
- value: valueProp,
91
- onChange: onChangeProp,
92
- defaultValue,
93
- });
94
- const clampedStepSize = Math.max(Math.min(stepSize, 10), 1);
86
+ ...rest
87
+ } = props;
95
88
 
96
- const focusOnAddButton = () => {
97
- addButtonRef.current?.focus();
98
- };
89
+ const addButtonRef = useRef<HTMLButtonElement>(null);
90
+ const { t } = useTranslation();
91
+ const recipe = useSlotRecipe({ recipe: numericStepperRecipe });
92
+ const styles = recipe();
93
+ const [value, onChange] = useControllableState<number>({
94
+ value: valueProp,
95
+ onChange: onChangeProp,
96
+ defaultValue,
97
+ });
98
+ const clampedStepSize = Math.max(Math.min(stepSize, 10), 1);
99
+
100
+ const focusOnAddButton = () => {
101
+ addButtonRef.current?.focus();
102
+ };
99
103
 
100
- return (
101
- <Field css={styles.root} flexDirection="row" width="auto">
102
- <VerySmallButton
103
- icon={<SubtractIcon stepLabel={clampedStepSize} />}
104
- aria-label={t(
105
- texts.decrementButtonAriaLabel(
106
- clampedStepSize,
107
- stepSize === 1
108
- ? ariaLabelContext.singular
109
- : ariaLabelContext.plural,
110
- ),
111
- )}
112
- onClick={() => {
113
- onChange(Math.max(value - clampedStepSize, minValue));
114
- if (Math.max(value - clampedStepSize, minValue) <= minValue) {
104
+ return (
105
+ <Field css={styles.root} width="auto" {...rest}>
106
+ <VerySmallButton
107
+ icon={<SubtractIcon stepLabel={clampedStepSize} />}
108
+ aria-label={t(
109
+ texts.decrementButtonAriaLabel(
110
+ clampedStepSize,
111
+ stepSize === 1
112
+ ? ariaLabelContext.singular
113
+ : ariaLabelContext.plural,
114
+ ),
115
+ )}
116
+ onClick={() => {
117
+ onChange(Math.max(value - clampedStepSize, minValue));
118
+ if (Math.max(value - clampedStepSize, minValue) <= minValue) {
119
+ focusOnAddButton();
120
+ }
121
+ }}
122
+ visibility={value <= minValue ? "hidden" : "visible"}
123
+ disabled={disabled}
124
+ id={value <= minValue ? undefined : idProp}
125
+ />
126
+ {withInput ? (
127
+ <Input
128
+ min={minValue}
129
+ max={maxValue}
130
+ name={nameProp}
131
+ value={value}
132
+ disabled={disabled}
133
+ id={!showZero && value === 0 ? undefined : idProp}
134
+ css={styles.input}
135
+ width={`${Math.max(value.toString().length + 1, 3)}ch`}
136
+ visibility={!showZero && value === 0 ? "hidden" : "visible"}
137
+ aria-live="assertive"
138
+ aria-label={
139
+ ariaLabelContext.plural === ""
140
+ ? ""
141
+ : t(texts.currentNumberAriaLabel(ariaLabelContext.plural))
142
+ }
143
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
144
+ const numericInput = Number(e.target.value);
145
+ if (Number.isNaN(numericInput)) {
146
+ return;
147
+ }
148
+ onChange(Math.max(Math.min(numericInput, maxValue), minValue));
149
+ if (
150
+ !showZero &&
151
+ Math.max(Math.min(numericInput, maxValue), minValue) === 0
152
+ ) {
115
153
  focusOnAddButton();
116
154
  }
117
155
  }}
118
- visibility={value <= minValue ? "hidden" : "visible"}
119
- disabled={disabled}
120
- id={value <= minValue ? undefined : idProp}
121
156
  />
122
- {withInput ? (
123
- <chakra.input
124
- min={minValue}
125
- max={maxValue}
126
- name={nameProp}
127
- value={value}
128
- disabled={disabled}
129
- id={!showZero && value === 0 ? undefined : idProp}
130
- css={styles.input}
131
- width={`${Math.max(value.toString().length + 1, 3)}ch`}
132
- visibility={!showZero && value === 0 ? "hidden" : "visible"}
133
- aria-live="assertive"
134
- aria-label={
135
- ariaLabelContext.plural === ""
136
- ? ""
137
- : t(texts.currentNumberAriaLabel(ariaLabelContext.plural))
138
- }
139
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
140
- const numericInput = Number(e.target.value);
141
- if (Number.isNaN(numericInput)) {
142
- return;
143
- }
144
- onChange(Math.max(Math.min(numericInput, maxValue), minValue));
145
- if (
146
- !showZero &&
147
- Math.max(Math.min(numericInput, maxValue), minValue) === 0
148
- ) {
149
- focusOnAddButton();
150
- }
151
- }}
152
- />
153
- ) : (
154
- <chakra.text
155
- css={styles}
156
- visibility={!showZero && value === 0 ? "hidden" : "visible"}
157
- aria-live="assertive"
158
- aria-label={
159
- ariaLabelContext.plural === ""
160
- ? ""
161
- : t(texts.currentNumberAriaLabel(ariaLabelContext.plural))
162
- }
163
- >
164
- {value}
165
- </chakra.text>
157
+ ) : (
158
+ <chakra.text
159
+ css={styles}
160
+ visibility={!showZero && value === 0 ? "hidden" : "visible"}
161
+ aria-live="assertive"
162
+ aria-label={
163
+ ariaLabelContext.plural === ""
164
+ ? ""
165
+ : t(texts.currentNumberAriaLabel(ariaLabelContext.plural))
166
+ }
167
+ >
168
+ {value}
169
+ </chakra.text>
170
+ )}
171
+ <VerySmallButton
172
+ ref={addButtonRef}
173
+ icon={<AddIcon stepLabel={clampedStepSize} />}
174
+ aria-label={t(
175
+ texts.incrementButtonAriaLabel(
176
+ clampedStepSize,
177
+ stepSize === 1
178
+ ? ariaLabelContext.singular
179
+ : ariaLabelContext.plural,
180
+ ),
166
181
  )}
167
- <VerySmallButton
168
- ref={addButtonRef}
169
- icon={<AddIcon stepLabel={clampedStepSize} />}
170
- aria-label={t(
171
- texts.incrementButtonAriaLabel(
172
- clampedStepSize,
173
- stepSize === 1
174
- ? ariaLabelContext.singular
175
- : ariaLabelContext.plural,
176
- ),
177
- )}
178
- onClick={() => onChange(Math.min(value + clampedStepSize, maxValue))}
179
- visibility={value >= maxValue ? "hidden" : "visible"}
180
- disabled={disabled}
181
- id={value >= maxValue ? undefined : idProp}
182
- />
183
- </Field>
184
- );
185
- },
186
- );
182
+ onClick={() => onChange(Math.min(value + clampedStepSize, maxValue))}
183
+ visibility={value >= maxValue ? "hidden" : "visible"}
184
+ disabled={disabled}
185
+ id={value >= maxValue ? undefined : idProp}
186
+ />
187
+ </Field>
188
+ );
189
+ });
187
190
  NumericStepper.displayName = "NumericStepper";
188
191
 
189
192
  type VerySmallButtonProps = {
@@ -5,7 +5,6 @@ import React, { forwardRef } from "react";
5
5
 
6
6
  import { ButtonProps, Input, InputProps } from "..";
7
7
  import { createTexts, useTranslation } from "..";
8
- import { InputGroupProps } from "./InputGroup";
9
8
 
10
9
  export interface PasswordVisibilityProps {
11
10
  /** Default visibility state */
@@ -19,7 +18,7 @@ export interface PasswordVisibilityProps {
19
18
  export interface PasswordInputProps
20
19
  extends InputProps,
21
20
  PasswordVisibilityProps {
22
- rootProps?: InputGroupProps;
21
+ rootProps?: InputProps;
23
22
  }
24
23
 
25
24
  /**
@@ -21,9 +21,12 @@ import { PropsWithChildren } from "react";
21
21
  import { CloseButton } from "@/button";
22
22
  import { selectSlotRecipe } from "@/theme/slot-recipes/select";
23
23
 
24
+ import { Field, FieldProps } from "./Field";
25
+
24
26
  type SelectVariantProps = RecipeVariantProps<typeof selectSlotRecipe>;
25
27
 
26
28
  export type SelectProps = ChakraSelectRootProps &
29
+ FieldProps &
27
30
  PropsWithChildren<SelectVariantProps> & {
28
31
  label?: string;
29
32
  };
@@ -58,25 +61,36 @@ export type SelectProps = ChakraSelectRootProps &
58
61
 
59
62
  export const Select = React.forwardRef<HTMLDivElement, SelectProps>(
60
63
  (props, ref) => {
61
- const { variant = "core", children, positioning, label, ...rest } = props;
64
+ const {
65
+ variant = "core",
66
+ children,
67
+ positioning,
68
+ label,
69
+ errorText,
70
+ invalid,
71
+ helperText,
72
+ ...rest
73
+ } = props;
62
74
  const recipe = useSlotRecipe({ key: "select" });
63
75
  const styles = recipe({ variant });
64
76
 
65
77
  return (
66
- <ChakraSelect.Root
67
- {...rest}
68
- ref={ref}
69
- positioning={{ sameWidth: true, ...positioning }}
70
- variant={variant}
71
- css={styles.root}
72
- position={"relative"}
73
- >
74
- <SelectTrigger data-attachable>
75
- <SelectValueText withPlaceholder={label ? true : false} />
76
- </SelectTrigger>
77
- {label && <SelectLabel css={styles.label}>{label}</SelectLabel>}
78
- <SelectContent css={styles.selectContent}>{children}</SelectContent>
79
- </ChakraSelect.Root>
78
+ <Field errorText={errorText} invalid={invalid} helperText={helperText}>
79
+ <ChakraSelect.Root
80
+ {...rest}
81
+ ref={ref}
82
+ positioning={{ sameWidth: true, ...positioning }}
83
+ variant={variant}
84
+ css={styles.root}
85
+ position={"relative"}
86
+ >
87
+ <SelectTrigger data-attachable>
88
+ <SelectValueText withPlaceholder={label ? true : false} />
89
+ </SelectTrigger>
90
+ {label && <SelectLabel css={styles.label}>{label}</SelectLabel>}
91
+ <SelectContent css={styles.selectContent}>{children}</SelectContent>
92
+ </ChakraSelect.Root>
93
+ </Field>
80
94
  );
81
95
  },
82
96
  );
@@ -8,7 +8,7 @@ import {
8
8
  import React, { forwardRef, PropsWithChildren } from "react";
9
9
 
10
10
  import { switchSlotRecipe } from "../theme/slot-recipes/switch";
11
- import { Field } from "./Field";
11
+ import { Field, FieldBaseProps } from "./Field";
12
12
 
13
13
  type SwitchVariants = RecipeVariantProps<typeof switchSlotRecipe>;
14
14
 
@@ -16,6 +16,7 @@ export type SwitchProps = Exclude<
16
16
  ChakraSwitch.RootProps,
17
17
  "size" | "colorPalette"
18
18
  > &
19
+ FieldBaseProps &
19
20
  PropsWithChildren<SwitchVariants> & {
20
21
  inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
21
22
  rootRef?: React.Ref<HTMLLabelElement>;
@@ -45,12 +46,25 @@ export type SwitchProps = Exclude<
45
46
  */
46
47
 
47
48
  export const Switch = forwardRef<HTMLInputElement, SwitchProps>((props) => {
48
- const { rootRef, size = "md", label, ...rest } = props;
49
+ const {
50
+ rootRef,
51
+ size = "md",
52
+ label,
53
+ invalid,
54
+ errorText,
55
+ helperText,
56
+ ...rest
57
+ } = props;
49
58
  const recipe = useSlotRecipe({ key: "switch" });
50
59
  const styles = recipe({ size });
51
60
 
52
61
  return (
53
- <Field style={{ width: "auto" }}>
62
+ <Field
63
+ style={{ width: "auto" }}
64
+ invalid={invalid}
65
+ errorText={errorText}
66
+ helperText={helperText}
67
+ >
54
68
  <ChakraSwitch.Root
55
69
  ref={rootRef}
56
70
  {...rest}
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
 
3
3
  import {
4
- FieldLabel,
5
4
  RecipeVariantProps,
6
5
  Textarea as ChakraTextarea,
7
6
  TextareaProps as ChakraTextareaProps,
@@ -18,6 +17,7 @@ import React, {
18
17
 
19
18
  import { textareaRecipe } from "../theme/recipes/textarea";
20
19
  import { Field, FieldProps } from "./Field";
20
+ import { FloatingLabel } from "./FloatingLabel";
21
21
 
22
22
  type TextareaVariants = RecipeVariantProps<typeof textareaRecipe>;
23
23
  export type TextareaProps = Exclude<
@@ -94,7 +94,7 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
94
94
  }
95
95
  placeholder=" "
96
96
  />
97
- <FieldLabel ref={labelRef}>{label}</FieldLabel>
97
+ <FloatingLabel ref={labelRef}>{label}</FloatingLabel>
98
98
  </Field>
99
99
  );
100
100
  },
@@ -12,33 +12,6 @@ export const fieldSlotRecipe = defineSlotRecipe({
12
12
  position: "relative",
13
13
  flexDirection: "column",
14
14
  },
15
- label: {
16
- /* For when input is filled */
17
- pos: "absolute",
18
- paddingX: 3,
19
- top: "0.3rem",
20
- fontWeight: "normal",
21
- fontSize: ["mobile.xs", "desktop.xs"],
22
- color: "text",
23
- pointerEvents: "none",
24
- transition: "position",
25
- zIndex: "docked",
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 */
34
- fontSize: ["mobile.xs", "desktop.xs"],
35
- color: "text",
36
- top: "0.3rem",
37
- },
38
- _disabled: {
39
- opacity: 0.4,
40
- },
41
- },
42
15
  requiredIndicator: {
43
16
  marginStart: 1,
44
17
  color: "brightRed",
@@ -58,7 +31,7 @@ export const fieldSlotRecipe = defineSlotRecipe({
58
31
  textStyle: "xs",
59
32
  width: "fit-content",
60
33
  position: "absolute",
61
- bottom: -4,
34
+ bottom: -5,
62
35
  left: 3,
63
36
  zIndex: "dropdown",
64
37
  maxWidth: "50ch",
@@ -7,16 +7,21 @@ export const numericStepperRecipe = defineSlotRecipe({
7
7
  className: "spor-numeric-stepper",
8
8
  base: {
9
9
  root: {
10
- display: "flex",
11
- flexDirection: "row",
12
- alignItems: "center",
10
+ "& > div": {
11
+ display: "flex",
12
+ flexDirection: "row",
13
+ alignItems: "center",
14
+ },
13
15
  },
14
16
  input: {
15
17
  fontSize: "sm",
16
18
  fontWeight: "bold",
17
19
  marginX: 0.5,
20
+ padding: 0,
18
21
  paddingX: 0.5,
19
22
  borderRadius: "xs",
23
+ outline: "none",
24
+ height: "auto",
20
25
  textAlign: "center",
21
26
  transitionProperty: "common",
22
27
  transitionDuration: "fast",
@@ -146,6 +146,10 @@ export const selectSlotRecipe = defineSlotRecipe({
146
146
  _open: {
147
147
  borderBottomRadius: 0,
148
148
  },
149
+ _invalid: {
150
+ outline: "2px solid",
151
+ outlineColor: "outline.error",
152
+ },
149
153
  },
150
154
  itemText: {
151
155
  flex: "1",
@@ -179,10 +183,6 @@ export const selectSlotRecipe = defineSlotRecipe({
179
183
  _active: {
180
184
  backgroundColor: "brand.surface.active",
181
185
  },
182
- _invalid: {
183
- outline: "2px solid",
184
- outlineColor: "outline.error",
185
- },
186
186
  _disabled: {
187
187
  pointerEvents: "none",
188
188
  color: "text.disabled",
@@ -1,67 +0,0 @@
1
- "use client";
2
-
3
- import type { GroupProps, InputElementProps } from "@chakra-ui/react";
4
- import { Group, InputElement } from "@chakra-ui/react";
5
- import * as React from "react";
6
-
7
- import { FieldLabel } from "./Field";
8
-
9
- export type InputGroupProps = GroupProps & {
10
- startElementProps?: InputElementProps;
11
- endElementProps?: InputElementProps;
12
- startElement?: React.ReactNode;
13
- endElement?: React.ReactNode;
14
- children: React.ReactElement;
15
- label?: string;
16
- };
17
-
18
- /**
19
- *
20
- * InputGroup is a wrapper for inputs that have a startElement and/or endElement.
21
- *
22
- * It is not exported to users, but used internally in the Input component.
23
- *
24
- */
25
-
26
- export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
27
- (props, ref) => {
28
- const {
29
- startElement,
30
- startElementProps,
31
- endElement,
32
- endElementProps,
33
- label,
34
- children,
35
- ...rest
36
- } = props;
37
-
38
- return (
39
- <Group ref={ref} {...rest}>
40
- {startElement && (
41
- <InputElement
42
- pointerEvents="none"
43
- paddingX={2}
44
- {...startElementProps}
45
- >
46
- {startElement}
47
- </InputElement>
48
- )}
49
- {React.cloneElement(children, {
50
- ...children.props,
51
- })}
52
- {label && (
53
- <FieldLabel left={startElement ? 4 : "0"} right={endElement ? 4 : 0}>
54
- {label}
55
- </FieldLabel>
56
- )}
57
-
58
- {endElement && (
59
- <InputElement placement="end" paddingX={2} {...endElementProps}>
60
- {endElement}
61
- </InputElement>
62
- )}
63
- </Group>
64
- );
65
- },
66
- );
67
- InputGroup.displayName = "InputGroup";