@vygruppen/spor-react 12.2.0 → 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.
@@ -0,0 +1,18 @@
1
+ import { defineStyle, Field, FieldLabelProps } from "@chakra-ui/react";
2
+
3
+ export const Label = (props: FieldLabelProps) => (
4
+ <Field.Label {...props} css={labelStyles} />
5
+ );
6
+
7
+ const labelStyles = defineStyle({
8
+ fontWeight: "normal",
9
+ paddingBottom: 1,
10
+ paddingX: 1,
11
+ fontSize: ["mobile.xs", "desktop.xs"],
12
+ color: "text",
13
+ pointerEvents: "none",
14
+ zIndex: "docked",
15
+ _disabled: {
16
+ opacity: 0.4,
17
+ },
18
+ });
@@ -9,7 +9,7 @@ import { DropdownDownFill18Icon } from "@vygruppen/spor-icon-react";
9
9
  import * as React from "react";
10
10
 
11
11
  import { nativeSelectSlotRecipe } from "../theme/slot-recipes/native-select";
12
- import { Field } from "./Field";
12
+ import { Field, FieldBaseProps } from "./Field";
13
13
 
14
14
  type NativeSelectVariantProps = RecipeVariantProps<
15
15
  typeof nativeSelectSlotRecipe
@@ -17,11 +17,9 @@ type NativeSelectVariantProps = RecipeVariantProps<
17
17
 
18
18
  export type NativeSelectdProps =
19
19
  React.PropsWithChildren<NativeSelectVariantProps> &
20
+ FieldBaseProps &
20
21
  ChakraNativeSelectFieldProps & {
21
22
  icon?: React.ReactNode;
22
- label: string;
23
- invalid?: boolean;
24
- disabled?: boolean;
25
23
  };
26
24
 
27
25
  /**
@@ -51,6 +49,9 @@ export const NativeSelect = React.forwardRef<
51
49
  label,
52
50
  invalid,
53
51
  disabled,
52
+ required,
53
+ helperText,
54
+ errorText,
54
55
  ...rest
55
56
  } = props;
56
57
 
@@ -58,7 +59,15 @@ export const NativeSelect = React.forwardRef<
58
59
  const styles = recipe({ variant });
59
60
 
60
61
  return (
61
- <Field label={label} invalid={invalid} disabled={disabled}>
62
+ <Field
63
+ label={label}
64
+ invalid={invalid}
65
+ disabled={disabled}
66
+ required={required}
67
+ helperText={helperText}
68
+ errorText={errorText}
69
+ floatingLabel={true}
70
+ >
62
71
  <ChakraNativeSelect.Root
63
72
  ref={ref}
64
73
  css={styles.root}
@@ -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
  },
@@ -267,3 +267,13 @@ export const datepickerAnatomy = createAnatomy("datepicker").parts(
267
267
  "box",
268
268
  "rangeCalendarPopover",
269
269
  );
270
+
271
+ export const checkboxCardAnatomy = createAnatomy("checkbox-card", [
272
+ "root",
273
+ "control",
274
+ "label",
275
+ "description",
276
+ "addon",
277
+ "indicator",
278
+ "content",
279
+ ]);
@@ -0,0 +1,183 @@
1
+ import { defineSlotRecipe } from "@chakra-ui/react";
2
+
3
+ import { checkboxCardAnatomy } from "./anatomy";
4
+
5
+ export const choiceChipSlotRecipe = defineSlotRecipe({
6
+ slots: checkboxCardAnatomy.keys(),
7
+ className: "chakra-checkbox-card",
8
+ base: {
9
+ root: {
10
+ display: "inline-flex",
11
+ alignItems: "center",
12
+ boxAlign: "center",
13
+ fontSize: "xs",
14
+ cursor: "pointer",
15
+ transitionProperty: "all",
16
+ borderRadius: "xl",
17
+ transitionDuration: "fast",
18
+ paddingInlineStart: "2",
19
+ paddingInlineEnd: "2",
20
+
21
+ outline: "1px solid",
22
+ outlineColor: "base.outline",
23
+ _checked: {
24
+ backgroundColor: "brand.surface",
25
+ borderRadius: "sm",
26
+ outline: "none",
27
+ color: "brand.text",
28
+ _hover: {
29
+ backgroundColor: "brand.surface.hover",
30
+ _active: {
31
+ backgroundColor: "brand.surface.active",
32
+ },
33
+ },
34
+ },
35
+
36
+ _focusVisible: {
37
+ outline: "2px solid",
38
+ outlineColor: "outline.focus",
39
+ outlineOffset: "1px",
40
+ },
41
+
42
+ _disabled: {
43
+ pointerEvents: "none",
44
+ boxShadow: "none",
45
+ backgroundColor: "surface.disabled",
46
+ color: "text.disabled",
47
+ outline: "none",
48
+
49
+ _hover: {
50
+ backgroundColor: "core.surface.disabled",
51
+ boxShadow: "none",
52
+ color: "core.text.disabled",
53
+ },
54
+ _checked: {
55
+ cursor: "not-allowed",
56
+ boxShadow: "none",
57
+ color: "core.text.disabled",
58
+ backgroundColor: "core.surface.disabled",
59
+ _hover: {
60
+ backgroundColor: "core.surface.disabled",
61
+ boxShadow: "none",
62
+ color: "core.text.disabled",
63
+ },
64
+ },
65
+ },
66
+ },
67
+
68
+ label: {
69
+ display: "flex",
70
+ alignItems: "center",
71
+
72
+ fontSize: "xs",
73
+ },
74
+ },
75
+
76
+ variants: {
77
+ size: {
78
+ xs: {
79
+ root: {
80
+ _checked: {
81
+ borderRadius: "0.563rem",
82
+ },
83
+ height: 5,
84
+ paddingX: 1.5,
85
+ },
86
+ },
87
+ sm: {
88
+ root: {
89
+ _checked: {
90
+ borderRadius: "sm",
91
+ },
92
+ height: 6,
93
+ paddingX: 2,
94
+ },
95
+ },
96
+ md: {
97
+ root: {
98
+ _checked: {
99
+ borderRadius: "sm",
100
+ },
101
+ height: 7,
102
+ paddingX: 2,
103
+ },
104
+ },
105
+ lg: {
106
+ root: {
107
+ _checked: {
108
+ borderRadius: "md",
109
+ },
110
+ height: 8,
111
+ paddingX: 3,
112
+ },
113
+ },
114
+ },
115
+
116
+ variant: {
117
+ core: {
118
+ root: {
119
+ color: "core.text",
120
+ outlineColor: "core.outline",
121
+
122
+ _hover: {
123
+ outline: "2px solid",
124
+ outlineColor: "core.outline.hover",
125
+
126
+ _active: {
127
+ outline: "1px solid",
128
+ outlineColor: "core.outline",
129
+ backgroundColor: "core.surface.active",
130
+ },
131
+ },
132
+ },
133
+ },
134
+ accent: {
135
+ root: {
136
+ backgroundColor: "accent.surface",
137
+ color: "accent.text",
138
+ outline: "none",
139
+
140
+ _hover: {
141
+ backgroundColor: "accent.surface.hover",
142
+
143
+ _active: {
144
+ backgroundColor: "accent.surface.active",
145
+ },
146
+ },
147
+ },
148
+ },
149
+ floating: {
150
+ root: {
151
+ backgroundColor: "floating.surface",
152
+ outline: "1px solid",
153
+ outlineColor: "floating.outline",
154
+ color: "floating.text",
155
+
156
+ boxShadow: "sm",
157
+ _hover: {
158
+ backgroundColor: "floating.surface.hover",
159
+ outline: "1px solid",
160
+ outlineColor: "floating.outline.hover",
161
+
162
+ _active: {
163
+ backgroundColor: "floating.surface.active",
164
+ outline: "1px solid",
165
+ outlineColor: "floating.outline",
166
+ },
167
+ },
168
+ },
169
+ },
170
+ },
171
+ chipType: {
172
+ icon: {},
173
+ choice: {},
174
+ filter: {},
175
+ },
176
+ },
177
+
178
+ defaultVariants: {
179
+ size: "md",
180
+ variant: "core",
181
+ chipType: "choice",
182
+ },
183
+ });