@vygruppen/spor-react 13.1.4 → 13.2.1

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": "13.1.4",
4
+ "version": "13.2.1",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -47,7 +47,7 @@
47
47
  "react-stately": "^3.31.1",
48
48
  "react-swipeable": "^7.0.1",
49
49
  "usehooks-ts": "^3.1.0",
50
- "@vygruppen/spor-design-tokens": "5.0.2",
50
+ "@vygruppen/spor-design-tokens": "5.0.3",
51
51
  "@vygruppen/spor-icon-react": "5.0.0",
52
52
  "@vygruppen/spor-loader": "0.7.0"
53
53
  },
@@ -9,6 +9,7 @@ import {
9
9
  import { IconComponent } from "@vygruppen/spor-icon-react";
10
10
 
11
11
  import { CloseButton } from "@/button";
12
+ import { createTexts, useTranslation } from "@/i18n";
12
13
 
13
14
  import { AlertIcon } from "./AlertIcon";
14
15
 
@@ -58,6 +59,7 @@ export const Alert = ({
58
59
  children,
59
60
  } = props;
60
61
  const { open, onClose } = useDisclosure({ defaultOpen: true });
62
+ const { t } = useTranslation();
61
63
 
62
64
  const handleAlertClose = () => {
63
65
  onClose();
@@ -67,9 +69,21 @@ export const Alert = ({
67
69
  const recipe = useSlotRecipe({ key: "alert" });
68
70
  const styles = recipe({ variant: props.variant });
69
71
 
72
+ const getAriaLabelText = () => {
73
+ const variant = props.variant;
74
+ if (variant === "important" || variant === "alt")
75
+ return texts.ariaLabelAlertWarning;
76
+ if (variant === "error" || variant === "error-secondary")
77
+ return texts.ariaLabelAlertError;
78
+ if (variant === "success") return texts.ariaLabelAlertSuccess;
79
+ return texts.ariaLabelAlertInformative;
80
+ };
81
+
82
+ const ariaLabel = t(getAriaLabelText());
83
+
70
84
  if (!open) return null;
71
85
  return (
72
- <ChakraAlert.Root ref={ref} {...props}>
86
+ <ChakraAlert.Root ref={ref} role="alert" aria-label={ariaLabel} {...props}>
73
87
  <ChakraAlert.Content
74
88
  flexDirection={title ? "column" : "row"}
75
89
  data-part="content"
@@ -114,3 +128,30 @@ export const Alert = ({
114
128
  </ChakraAlert.Root>
115
129
  );
116
130
  };
131
+
132
+ const texts = createTexts({
133
+ ariaLabelAlertInformative: {
134
+ en: "Announcement",
135
+ nb: "Kunngjøring",
136
+ nn: "Kunngjering",
137
+ sv: "Meddelande",
138
+ },
139
+ ariaLabelAlertWarning: {
140
+ en: "Warning",
141
+ nb: "Advarsel",
142
+ nn: "Varsel",
143
+ sv: "Varning",
144
+ },
145
+ ariaLabelAlertError: {
146
+ en: "Error",
147
+ nb: "Feil",
148
+ nn: "Feil",
149
+ sv: "Fel",
150
+ },
151
+ ariaLabelAlertSuccess: {
152
+ en: "Success",
153
+ nb: "Suksess",
154
+ nn: "Suksess",
155
+ sv: "Framgång",
156
+ },
157
+ });
@@ -1,94 +1,156 @@
1
1
  "use client";
2
- import { CheckboxCard, CheckboxCardRootProps, Span } from "@chakra-ui/react";
3
- import { CloseOutline24Icon } from "@vygruppen/spor-icon-react";
4
- import React from "react";
2
+ import {
3
+ RadioCard as ChakraRadioCard,
4
+ RecipeVariantProps,
5
+ Span,
6
+ useSlotRecipe,
7
+ } from "@chakra-ui/react";
8
+ import { createContext, useContext, useId } from "react";
5
9
 
6
- type CheckBoxIcon = {
7
- default: React.ReactNode;
8
- checked: React.ReactNode;
9
- };
10
-
11
- export type ChoiceChipProps = Omit<
12
- CheckboxCardRootProps,
13
- "onCheckedChange" | "checked"
14
- > & {
15
- icon?: CheckBoxIcon;
16
- onCheckedChange?: (checked: boolean) => void;
17
- checked?: boolean;
18
- };
10
+ import { choiceChipSlotRecipe } from "@/theme/slot-recipes/choice-chip";
19
11
 
20
12
  /**
21
- * Choice chips are checkboxes that look like selectable buttons.
13
+ * Choice chips are radio buttons that look like selectable buttons, allowing only one selection at a time.
22
14
  *
23
15
  * Choice chips are available in four different sizes - `xs`, `sm`, `md` and `lg`.
24
16
  *
25
17
  * ```tsx
26
- * <Stack flexDirection="row">
27
- * <ChoiceChip size="lg">Bus</ChoiceChip>
28
- * <ChoiceChip size="lg">Train</ChoiceChip>
29
- * </Stack>
18
+ * <ChoiceChipGroup defaultValue="economy">
19
+ * <ChoiceChip value="economy">Economy</ChoiceChip>
20
+ * <ChoiceChip value="business">Business</ChoiceChip>
21
+ * <ChoiceChip value="first-class">First Class</ChoiceChip>
22
+ * </ChoiceChipGroup>
30
23
  * ```
31
24
  *
32
- * There are also three different chipType - `icon`, `choice` and `filter`.
33
- *
34
- * ```tsx
35
- * <Stack flexDirection="row">
36
- * <ChoiceChip chipType="icon" icon={<Bus24Icon />}>Bus</ChoiceChip>
37
- * <ChoiceChip chipType="choice" icon={<Bus24Icon />}>Bus</ChoiceChip>
38
- * <ChoiceChip chipType="filter" icon={<Bus24Icon />}>Bus</ChoiceChip>
39
- * </Stack>
40
- *
41
25
  * There are also three different variants - `core`, `accent` and `floating`.
42
26
  *
43
27
  * ```tsx
44
- * <Stack flexDirection="row">
45
- * <ChoiceChip variant="core">Bus</ChoiceChip>
46
- * <ChoiceChip variant="accent">Boat</ChoiceChip>
47
- * <ChoiceChip variant="floating">Train</ChoiceChip>
48
- * </Stack>
28
+ * <>
29
+ * <ChoiceChipGroup defaultValue="bus" variant="core">
30
+ * <ChoiceChip value="bus">Bus</ChoiceChip>
31
+ * </ChoiceChipGroup>
32
+ * <ChoiceChipGroup defaultValue="bus" variant="accent">
33
+ * <ChoiceChip value="bus">Bus</ChoiceChip>
34
+ * </ChoiceChipGroup>
35
+ * <ChoiceChipGroup defaultValue="bus" variant="floating">
36
+ * <ChoiceChip value="bus">Bus</ChoiceChip>
37
+ * </ChoiceChipGroup>
38
+ * </>
49
39
  * ```
40
+ *
41
+ * @see https://spor.vy.no/components/choice-chip
50
42
  */
51
43
 
44
+ type RadioCardVariantProps = RecipeVariantProps<typeof choiceChipSlotRecipe>;
45
+
46
+ type Variant = Pick<RadioCardRootProps, "variant" | "size">;
47
+
48
+ const ChoiceChipContext = createContext<Variant>({
49
+ variant: "core",
50
+ size: "sm",
51
+ });
52
+ const useChoiceChipContext = () => useContext(ChoiceChipContext);
53
+
54
+ type CheckBoxIcon = {
55
+ default: React.ReactNode;
56
+ checked: React.ReactNode;
57
+ };
58
+
59
+ type RadioCardItemProps = Exclude<
60
+ ChakraRadioCard.ItemProps,
61
+ "colorPalette" | "indicator" | "variant" | "size" | "addon"
62
+ > &
63
+ RadioCardVariantProps & {
64
+ inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
65
+ ariaLabel?: string;
66
+ icon?: CheckBoxIcon;
67
+ };
68
+
52
69
  export const ChoiceChip = ({
53
70
  ref,
54
- children,
55
- icon,
56
- onCheckedChange,
57
- ...rootProps
58
- }: ChoiceChipProps & {
71
+ ...props
72
+ }: RadioCardItemProps & {
59
73
  ref?: React.Ref<HTMLInputElement>;
60
74
  }) => {
75
+ const { children, inputProps, icon, variant, size, css, ...rest } = props;
76
+ const { variant: contextVariant, size: contextSize } = useChoiceChipContext();
77
+
78
+ const uniqueId = useId();
79
+ const itemControlId = `radio-card-item-control-${uniqueId}`;
80
+
81
+ const inputHasAriaLabel =
82
+ inputProps?.["aria-labelledby"] || inputProps?.["aria-label"];
83
+
84
+ const finalVariant = variant ?? contextVariant;
85
+ const finalSize = size ?? contextSize;
86
+
87
+ const recipe = useSlotRecipe({ key: "choiceChip" });
88
+ const styles = recipe({
89
+ variant: finalVariant,
90
+ size: finalSize,
91
+ });
92
+
93
+ return (
94
+ <ChakraRadioCard.Item {...rest} css={{ ...css, ...styles.item }}>
95
+ <ChakraRadioCard.ItemHiddenInput
96
+ aria-labelledby={
97
+ inputHasAriaLabel ? inputProps?.["aria-labelledby"] : itemControlId
98
+ }
99
+ ref={ref}
100
+ {...inputProps}
101
+ />
102
+ <ChakraRadioCard.ItemControl
103
+ id={itemControlId}
104
+ aria-hidden
105
+ css={styles.itemControl}
106
+ >
107
+ <ChakraRadioCard.ItemContext>
108
+ {({ checked }) => (
109
+ <ChakraRadioCard.Label css={styles.label}>
110
+ {checked
111
+ ? icon?.checked && <Span>{icon.checked}</Span>
112
+ : icon?.default && <Span>{icon.default}</Span>}
113
+ {children}
114
+ </ChakraRadioCard.Label>
115
+ )}
116
+ </ChakraRadioCard.ItemContext>
117
+ </ChakraRadioCard.ItemControl>
118
+ </ChakraRadioCard.Item>
119
+ );
120
+ };
121
+
122
+ type RadioCardRootProps = Exclude<
123
+ ChakraRadioCard.RootProps,
124
+ "variant" | "size"
125
+ > &
126
+ RadioCardVariantProps & {
127
+ children: React.ReactNode;
128
+ gap?: string | number;
129
+ direction?: "row" | "column";
130
+ display?: string;
131
+ };
132
+
133
+ export const ChoiceChipGroup = ({
134
+ ref,
135
+ ...props
136
+ }: RadioCardRootProps & {
137
+ ref?: React.Ref<HTMLDivElement>;
138
+ }) => {
139
+ const { children, variant, size, css, ...rest } = props;
140
+ const recipe = useSlotRecipe({ key: "choiceChip" });
141
+ const styles = recipe({ variant, size });
61
142
  return (
62
- <CheckboxCard.Root
63
- {...rootProps}
64
- {...(onCheckedChange && {
65
- onCheckedChange: (details) => onCheckedChange(!!details.checked),
66
- })}
67
- >
68
- <CheckboxCard.Context>
69
- {({ checked }) => (
70
- <>
71
- <CheckboxCard.HiddenInput ref={ref} />
72
- <CheckboxCard.Control>
73
- <CheckboxCard.Content>
74
- <CheckboxCard.Label>
75
- {icon && (
76
- <Span width="24px">
77
- {checked ? icon.checked : icon.default}
78
- </Span>
79
- )}
80
-
81
- {rootProps.chipType !== "icon" && children}
82
-
83
- {rootProps.chipType === "filter" && checked && (
84
- <CloseOutline24Icon />
85
- )}
86
- </CheckboxCard.Label>
87
- </CheckboxCard.Content>
88
- </CheckboxCard.Control>
89
- </>
90
- )}
91
- </CheckboxCard.Context>
92
- </CheckboxCard.Root>
143
+ <ChoiceChipContext.Provider value={{ variant, size }}>
144
+ <ChakraRadioCard.Root
145
+ css={{ ...styles.root, ...css }}
146
+ ref={ref}
147
+ variant={variant}
148
+ {...rest}
149
+ >
150
+ {children}
151
+ </ChakraRadioCard.Root>
152
+ </ChoiceChipContext.Provider>
93
153
  );
94
154
  };
155
+
156
+ export const ChoiceChipLabel = ChakraRadioCard.Label;
@@ -54,6 +54,7 @@ export type FieldBaseProps = {
54
54
  floatingLabel?: boolean;
55
55
  shouldFloat?: boolean;
56
56
  labelAsChild?: boolean;
57
+ gap?: string | number;
57
58
  };
58
59
 
59
60
  export type FieldProps = Omit<
@@ -100,13 +101,14 @@ export const Field = ({
100
101
  id,
101
102
  shouldFloat,
102
103
  labelAsChild,
104
+ gap,
103
105
  ...rest
104
106
  } = props;
105
107
  const recipe = useSlotRecipe({ key: "field" });
106
108
  const styles = recipe();
107
109
 
108
110
  return (
109
- <Stack gap="2" ref={ref} width="100%" {...rest}>
111
+ <Stack ref={ref} width="100%" {...rest}>
110
112
  <ChakraField.Root
111
113
  disabled={disabled}
112
114
  invalid={invalid}
@@ -115,6 +117,7 @@ export const Field = ({
115
117
  css={styles.root}
116
118
  direction={direction}
117
119
  id={id}
120
+ gap={gap}
118
121
  >
119
122
  {label && !floatingLabel && (
120
123
  <Label asChild={labelAsChild} aria-hidden>
@@ -0,0 +1,85 @@
1
+ "use client";
2
+ import { CheckboxCard, CheckboxCardRootProps, Span } from "@chakra-ui/react";
3
+ import { CloseOutline24Icon } from "@vygruppen/spor-icon-react";
4
+ import React from "react";
5
+
6
+ type CheckBoxIcon = {
7
+ default: React.ReactNode;
8
+ checked: React.ReactNode;
9
+ };
10
+
11
+ export type FilterChipProps = Omit<
12
+ CheckboxCardRootProps,
13
+ "onCheckedChange" | "checked"
14
+ > & {
15
+ icon?: CheckBoxIcon;
16
+ onCheckedChange?: (checked: boolean) => void;
17
+ checked?: boolean;
18
+ };
19
+
20
+ /**
21
+ * Filter chips are checkboxes that look like selectable buttons.
22
+ *
23
+ * Filter chips are available in four different sizes - `xs`, `sm`, `md` and `lg`.
24
+ *
25
+ * ```tsx
26
+ * <Stack flexDirection="row">
27
+ * <FilterChip size="lg">Bus</FilterChip>
28
+ * <FilterChip size="lg">Train</FilterChip>
29
+ * </Stack>
30
+ * ```
31
+ *
32
+ * There are also three different variants - `core`, `accent` and `floating`.
33
+ *
34
+ * ```tsx
35
+ * <Stack flexDirection="row">
36
+ * <FilterChip variant="core">Bus</FilterChip>
37
+ * <FilterChip variant="accent">Boat</FilterChip>
38
+ * <FilterChip variant="floating">Train</FilterChip>
39
+ * </Stack>
40
+ * ```
41
+ *
42
+ * @see https://spor.vy.no/components/filter-chip
43
+ */
44
+
45
+ export const FilterChip = ({
46
+ ref,
47
+ children,
48
+ icon,
49
+ onCheckedChange,
50
+ ...rootProps
51
+ }: FilterChipProps & {
52
+ ref?: React.Ref<HTMLInputElement>;
53
+ }) => {
54
+ return (
55
+ <CheckboxCard.Root
56
+ {...rootProps}
57
+ {...(onCheckedChange && {
58
+ onCheckedChange: (details) => onCheckedChange(!!details.checked),
59
+ })}
60
+ >
61
+ <CheckboxCard.Context>
62
+ {({ checked }) => (
63
+ <>
64
+ <CheckboxCard.HiddenInput ref={ref} />
65
+ <CheckboxCard.Control>
66
+ <CheckboxCard.Content>
67
+ <CheckboxCard.Label>
68
+ {checked
69
+ ? icon?.checked && <Span>{icon.checked}</Span>
70
+ : icon?.default && <Span>{icon.default}</Span>}
71
+
72
+ {rootProps.chipType !== "icon" && children}
73
+
74
+ {rootProps.chipType === "filter" && checked && (
75
+ <CloseOutline24Icon />
76
+ )}
77
+ </CheckboxCard.Label>
78
+ </CheckboxCard.Content>
79
+ </CheckboxCard.Control>
80
+ </>
81
+ )}
82
+ </CheckboxCard.Context>
83
+ </CheckboxCard.Root>
84
+ );
85
+ };
@@ -87,6 +87,7 @@ export const NumericStepper = ({
87
87
  label,
88
88
  helperText,
89
89
  errorText,
90
+ gap,
90
91
  ...rest
91
92
  } = props;
92
93
 
@@ -116,6 +117,7 @@ export const NumericStepper = ({
116
117
  invalid={invalid}
117
118
  readOnly={readOnly}
118
119
  required={required}
120
+ gap={gap}
119
121
  >
120
122
  <VerySmallButton
121
123
  icon={<SubtractIcon stepLabel={clampedStepSize} />}
@@ -167,7 +169,10 @@ export const NumericStepper = ({
167
169
  ) : (
168
170
  <Text
169
171
  aria-live="assertive"
170
- paddingX="0.95rem"
172
+ width={`${Math.max(value.toString().length + 1, 3)}ch`}
173
+ paddingX={0.5}
174
+ padding={0}
175
+ textAlign="center"
171
176
  aria-label={
172
177
  ariaLabelContext.plural === ""
173
178
  ? ""
@@ -7,6 +7,7 @@ export * from "./ChoiceChip";
7
7
  export * from "./Combobox";
8
8
  export * from "./Field";
9
9
  export * from "./Fieldset";
10
+ export * from "./FilterChip";
10
11
  export * from "./Input";
11
12
  export * from "./InputChip";
12
13
  export * from "./ListBox";
@@ -66,24 +66,40 @@ export const badgeRecipie = defineRecipe({
66
66
  color: "icon.critical",
67
67
  },
68
68
  },
69
+ brightRed: {
70
+ backgroundColor: {
71
+ _light: "brightRed",
72
+ _dark: "brightRed",
73
+ },
74
+ color: {
75
+ _light: "pink",
76
+ _dark: "pink",
77
+ },
78
+ "& svg": {
79
+ color: {
80
+ _light: "pink",
81
+ _dark: "pink",
82
+ },
83
+ },
84
+ },
69
85
  },
70
86
  size: {
71
87
  sm: {
72
- fontSize: "desktop.xs",
88
+ fontSize: "desktop.2xs",
73
89
  paddingX: "0.5",
74
90
  paddingY: "0",
75
91
  fontWeight: "normal",
76
92
  borderRadius: "xxs",
77
93
  },
78
94
  md: {
79
- fontSize: "desktop.xs",
95
+ fontSize: "desktop.2xs",
80
96
  paddingX: "1",
81
97
  paddingY: "0.5",
82
98
  fontWeight: "bold",
83
99
  borderRadius: "xs",
84
100
  },
85
101
  lg: {
86
- fontSize: "desktop.sm",
102
+ fontSize: "desktop.xs",
87
103
  paddingX: "1.5",
88
104
  paddingY: "0.5",
89
105
  fontWeight: "bold",
@@ -64,13 +64,13 @@ export const pressableCardRecipe = defineRecipe({
64
64
  accent: {
65
65
  boxShadow: "0px 1px 3px 0px var(--shadow-color)",
66
66
  shadowColor: "surface.disabled",
67
- background: "surface.success",
67
+ background: "surface.accent",
68
68
  _hover: {
69
- background: "surface.success.hover",
69
+ background: "surface.accent.hover",
70
70
 
71
71
  boxShadow: "0px 2px 6px 0px var(--shadow-color)",
72
72
  _active: {
73
- background: "surface.success.active",
73
+ background: "surface.accent.active",
74
74
  boxShadow: "none",
75
75
  },
76
76
  },
@@ -158,6 +158,7 @@ export const radioCardAnatomy = createAnatomy("radio-card").parts(
158
158
  "itemText",
159
159
  "itemDescription",
160
160
  "itemContent",
161
+ "itemControl",
161
162
  );
162
163
 
163
164
  export const radioAnatomy = createAnatomy("radio").parts(