@okta/odyssey-react-mui 1.6.13 → 1.6.17

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,33 @@
1
+ /*!
2
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
3
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
4
+ *
5
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ * Unless required by applicable law or agreed to in writing, software
7
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
8
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ *
10
+ * See the License for the specific language governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { useEffect, useRef, useState } from "react";
14
+ export const useControlledState = _ref => {
15
+ let {
16
+ controlledValue,
17
+ uncontrolledValue
18
+ } = _ref;
19
+ const isControlledMode = useRef(controlledValue !== undefined);
20
+ const [stateValue, setStateValue] = useState(isControlledMode.current ? controlledValue : uncontrolledValue);
21
+ useEffect(() => {
22
+ if (isControlledMode.current) {
23
+ setStateValue(controlledValue);
24
+ }
25
+ }, [controlledValue]);
26
+ const setState = value => {
27
+ if (!isControlledMode.current) {
28
+ setStateValue(value);
29
+ }
30
+ };
31
+ return [stateValue, setState];
32
+ };
33
+ //# sourceMappingURL=useControlledState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useControlledState.js","names":["useEffect","useRef","useState","useControlledState","_ref","controlledValue","uncontrolledValue","isControlledMode","undefined","stateValue","setStateValue","current","setState","value"],"sources":["../src/useControlledState.ts"],"sourcesContent":["/*!\n * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.\n * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the \"License.\")\n *\n * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *\n * See the License for the specific language governing permissions and limitations under the License.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\n\ntype UseControlledStateProps<Value> = {\n controlledValue?: Value; // isChecked\n uncontrolledValue?: Value; // isDefaultChecked\n};\n\n/**\n * Use the same way as `useState`. Returns a stateful value, and a function to update it.\n * When `initialState` is passed, the returned function to update it does nothing. This is\n * useful to handle values in components that may be controlled externally when that value is\n * passed in props and thus wish to prevent internal updates of the same value.\n *\n * @param initialState\n * @see https://react.dev/reference/react/useState\n */\nexport const useControlledState = <Value>({\n controlledValue,\n uncontrolledValue,\n}: UseControlledStateProps<Value>) => {\n const isControlledMode = useRef(controlledValue !== undefined);\n const [stateValue, setStateValue] = useState(\n isControlledMode.current ? controlledValue : uncontrolledValue\n );\n\n useEffect(() => {\n if (isControlledMode.current) {\n setStateValue(controlledValue);\n }\n }, [controlledValue]);\n\n const setState: typeof setStateValue = (value) => {\n if (!isControlledMode.current) {\n setStateValue(value);\n }\n };\n\n return [\n stateValue,\n // If `value` is controlled externally, ignore calls to the setter.\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n setState,\n ] as const;\n};\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAASA,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,OAAO;AAgBnD,OAAO,MAAMC,kBAAkB,GAAGC,IAAA,IAGI;EAAA,IAHI;IACxCC,eAAe;IACfC;EAC8B,CAAC,GAAAF,IAAA;EAC/B,MAAMG,gBAAgB,GAAGN,MAAM,CAACI,eAAe,KAAKG,SAAS,CAAC;EAC9D,MAAM,CAACC,UAAU,EAAEC,aAAa,CAAC,GAAGR,QAAQ,CAC1CK,gBAAgB,CAACI,OAAO,GAAGN,eAAe,GAAGC,iBAC/C,CAAC;EAEDN,SAAS,CAAC,MAAM;IACd,IAAIO,gBAAgB,CAACI,OAAO,EAAE;MAC5BD,aAAa,CAACL,eAAe,CAAC;IAChC;EACF,CAAC,EAAE,CAACA,eAAe,CAAC,CAAC;EAErB,MAAMO,QAA8B,GAAIC,KAAK,IAAK;IAChD,IAAI,CAACN,gBAAgB,CAACI,OAAO,EAAE;MAC7BD,aAAa,CAACG,KAAK,CAAC;IACtB;EACF,CAAC;EAED,OAAO,CACLJ,UAAU,EAGVG,QAAQ,CACT;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okta/odyssey-react-mui",
3
- "version": "1.6.13",
3
+ "version": "1.6.17",
4
4
  "description": "React MUI components for Odyssey, Okta's design system",
5
5
  "author": "Okta, Inc.",
6
6
  "license": "Apache-2.0",
@@ -51,7 +51,7 @@
51
51
  "@mui/system": "^5.14.9",
52
52
  "@mui/utils": "^5.11.2",
53
53
  "@mui/x-date-pickers": "^5.0.15",
54
- "@okta/odyssey-design-tokens": "1.6.13",
54
+ "@okta/odyssey-design-tokens": "1.6.17",
55
55
  "date-fns": "^2.30.0",
56
56
  "i18next": "^23.5.1",
57
57
  "material-react-table": "^1.14.0",
@@ -63,5 +63,5 @@
63
63
  "react": ">=17 <19",
64
64
  "react-dom": ">=17 <19"
65
65
  },
66
- "gitHead": "34fa5cf5ebe53fde24cdc505b90107624c74c0fe"
66
+ "gitHead": "7643c5f6af8bd8b9a90c66a1d298abe7c0b2ca79"
67
67
  }
package/src/Checkbox.tsx CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { useTranslation } from "react-i18next";
14
- import { memo, useCallback, useMemo, useState } from "react";
14
+ import { memo, useCallback, useMemo } from "react";
15
15
  import {
16
16
  Checkbox as MuiCheckbox,
17
17
  CheckboxProps as MuiCheckboxProps,
@@ -21,6 +21,8 @@ import {
21
21
  import { FieldComponentProps } from "./FieldComponentProps";
22
22
  import { Typography } from "./Typography";
23
23
  import type { SeleniumProps } from "./SeleniumProps";
24
+ import { useControlledState } from "./useControlledState";
25
+ import { CheckedFieldProps } from "./FormCheckedProps";
24
26
 
25
27
  export const checkboxValidityValues = ["valid", "invalid", "inherit"] as const;
26
28
 
@@ -34,9 +36,13 @@ export type CheckboxProps = {
34
36
  */
35
37
  ariaLabelledBy?: string;
36
38
  /**
37
- * Determines whether the Checkbox is checked
39
+ * The id of the `input` element.
38
40
  */
39
- isDefaultChecked?: boolean;
41
+ id?: string;
42
+ /**
43
+ * Determines whether the Checkbox is disabled
44
+ */
45
+ isDisabled?: boolean;
40
46
  /**
41
47
  * Determines whether the Checkbox is in an indeterminate state
42
48
  */
@@ -49,10 +55,6 @@ export type CheckboxProps = {
49
55
  * The label text for the Checkbox
50
56
  */
51
57
  label?: string;
52
- /**
53
- * The change event handler for the Checkbox
54
- */
55
- onChange?: MuiCheckboxProps["onChange"];
56
58
  /**
57
59
  * The checkbox validity, if different from its enclosing group. Defaults to "inherit".
58
60
  */
@@ -62,13 +64,15 @@ export type CheckboxProps = {
62
64
  */
63
65
  value?: string;
64
66
  } & Pick<FieldComponentProps, "id" | "isDisabled" | "name"> &
67
+ CheckedFieldProps<MuiCheckboxProps> &
65
68
  SeleniumProps;
66
69
 
67
70
  const Checkbox = ({
68
71
  ariaLabel,
69
72
  ariaLabelledBy,
70
73
  id: idOverride,
71
- isDefaultChecked = false,
74
+ isChecked,
75
+ isDefaultChecked,
72
76
  isDisabled,
73
77
  isIndeterminate,
74
78
  isRequired,
@@ -80,7 +84,10 @@ const Checkbox = ({
80
84
  value,
81
85
  }: CheckboxProps) => {
82
86
  const { t } = useTranslation();
83
- const [isCheckedValue, setIsCheckedValue] = useState(isDefaultChecked);
87
+ const [isLocalChecked, setIsLocalChecked] = useControlledState({
88
+ controlledValue: isChecked,
89
+ uncontrolledValue: isDefaultChecked,
90
+ });
84
91
 
85
92
  const label = useMemo(() => {
86
93
  if (isRequired) {
@@ -97,19 +104,18 @@ const Checkbox = ({
97
104
  }
98
105
  }, [isRequired, labelProp, t]);
99
106
 
100
- const onChange = useCallback(
107
+ const onChange = useCallback<NonNullable<MuiCheckboxProps["onChange"]>>(
101
108
  (event, checked) => {
102
- setIsCheckedValue(event.target.checked);
109
+ setIsLocalChecked(checked);
103
110
  onChangeProp?.(event, checked);
104
111
  },
105
- [onChangeProp]
112
+ [onChangeProp, setIsLocalChecked]
106
113
  );
107
114
 
108
115
  return (
109
116
  <FormControlLabel
110
117
  aria-label={ariaLabel}
111
118
  aria-labelledby={ariaLabelledBy}
112
- checked={isCheckedValue}
113
119
  className={
114
120
  validity === "invalid"
115
121
  ? "Mui-error"
@@ -118,14 +124,18 @@ const Checkbox = ({
118
124
  : ""
119
125
  }
120
126
  control={
121
- <MuiCheckbox indeterminate={isIndeterminate} required={isRequired} />
127
+ <MuiCheckbox
128
+ checked={isLocalChecked}
129
+ indeterminate={isIndeterminate}
130
+ onChange={onChange}
131
+ required={isRequired}
132
+ />
122
133
  }
123
134
  data-se={testId}
124
135
  disabled={isDisabled}
125
136
  id={idOverride}
126
137
  label={label}
127
138
  name={nameOverride ?? idOverride}
128
- onChange={onChange}
129
139
  value={value}
130
140
  required={isRequired}
131
141
  />
@@ -0,0 +1,59 @@
1
+ /*!
2
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
3
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
4
+ *
5
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ * Unless required by applicable law or agreed to in writing, software
7
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
8
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ *
10
+ * See the License for the specific language governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { SwitchBaseProps } from "@mui/material/internal/SwitchBase";
14
+
15
+ type SwitchOnChangeProp = Pick<SwitchBaseProps, "onChange">;
16
+
17
+ export type ControlledCheckedFieldProps<
18
+ TogglableInputElement extends SwitchOnChangeProp
19
+ > = {
20
+ /**
21
+ * Sets the checked state of the Checkbox
22
+ */
23
+ isChecked: boolean;
24
+ /**
25
+ * Determines whether the Checkbox is checked
26
+ * Should not be used if `isChecked` is used
27
+ */
28
+ isDefaultChecked?: never;
29
+ /**
30
+ * The change event handler for the Checkbox
31
+ * Must be used if `isChecked` is used
32
+ */
33
+ onChange: TogglableInputElement["onChange"];
34
+ };
35
+
36
+ export type UncontrolledCheckedFieldProps<
37
+ TogglableInputElement extends SwitchOnChangeProp
38
+ > = {
39
+ /**
40
+ * Sets the checked state of the Checkbox
41
+ */
42
+ isChecked?: never;
43
+ /**
44
+ * Determines whether the Checkbox is checked
45
+ * Should not be used if `isChecked` is used
46
+ */
47
+ isDefaultChecked?: boolean;
48
+ /**
49
+ * The change event handler for the Checkbox
50
+ * Must be used if `isChecked` is used
51
+ */
52
+ onChange?: TogglableInputElement["onChange"];
53
+ };
54
+
55
+ export type CheckedFieldProps<
56
+ TogglableInputElement extends SwitchOnChangeProp
57
+ > =
58
+ | ControlledCheckedFieldProps<TogglableInputElement>
59
+ | UncontrolledCheckedFieldProps<TogglableInputElement>;
package/src/Radio.tsx CHANGED
@@ -10,10 +10,12 @@
10
10
  * See the License for the specific language governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import { Radio as MuiRadio } from "@mui/material";
14
- import { memo } from "react";
15
-
16
- import { FormControlLabel } from "@mui/material";
13
+ import {
14
+ FormControlLabel,
15
+ Radio as MuiRadio,
16
+ RadioProps as MuiRadioProps,
17
+ } from "@mui/material";
18
+ import { memo, useCallback } from "react";
17
19
 
18
20
  import { FieldComponentProps } from "./FieldComponentProps";
19
21
  import type { SeleniumProps } from "./SeleniumProps";
@@ -35,6 +37,10 @@ export type RadioProps = {
35
37
  * The value attribute of the Radio
36
38
  */
37
39
  value: string;
40
+ /**
41
+ * Callback fired when the state is changed. Provides event and checked value.
42
+ */
43
+ onChange?: MuiRadioProps["onChange"];
38
44
  } & Pick<FieldComponentProps, "isDisabled" | "name"> &
39
45
  SeleniumProps;
40
46
 
@@ -46,18 +52,28 @@ const Radio = ({
46
52
  name,
47
53
  testId,
48
54
  value,
49
- }: RadioProps) => (
50
- <FormControlLabel
51
- checked={isChecked}
52
- className={isInvalid ? "Mui-error" : ""}
53
- control={<MuiRadio />}
54
- data-se={testId}
55
- disabled={isDisabled}
56
- label={label}
57
- name={name}
58
- value={value}
59
- />
60
- );
55
+ onChange: onChangeProp,
56
+ }: RadioProps) => {
57
+ const onChange = useCallback<NonNullable<MuiRadioProps["onChange"]>>(
58
+ (event, checked) => {
59
+ onChangeProp?.(event, checked);
60
+ },
61
+ [onChangeProp]
62
+ );
63
+
64
+ return (
65
+ <FormControlLabel
66
+ checked={isChecked}
67
+ className={isInvalid ? "Mui-error" : ""}
68
+ control={<MuiRadio onChange={onChange} />}
69
+ data-se={testId}
70
+ disabled={isDisabled}
71
+ label={label}
72
+ name={name}
73
+ value={value}
74
+ />
75
+ );
76
+ };
61
77
 
62
78
  const MemoizedRadio = memo(Radio);
63
79
  MemoizedRadio.displayName = "Radio";
@@ -20,6 +20,7 @@ import { Radio, RadioProps } from "./Radio";
20
20
  import { Field } from "./Field";
21
21
  import { FieldComponentProps } from "./FieldComponentProps";
22
22
  import type { SeleniumProps } from "./SeleniumProps";
23
+ import { useControlledState } from "./useControlledState";
23
24
 
24
25
  export type RadioGroupProps = {
25
26
  /**
@@ -57,10 +58,22 @@ const RadioGroup = ({
57
58
  isDisabled,
58
59
  label,
59
60
  name: nameOverride,
60
- onChange,
61
+ onChange: onChangeProp,
61
62
  testId,
62
63
  value,
63
64
  }: RadioGroupProps) => {
65
+ const [localValue, setLocalValue] = useControlledState({
66
+ controlledValue: value,
67
+ uncontrolledValue: defaultValue,
68
+ });
69
+
70
+ const onChange = useCallback<NonNullable<MuiRadioGroupProps["onChange"]>>(
71
+ (event, value) => {
72
+ setLocalValue(value);
73
+ onChangeProp?.(event, value);
74
+ },
75
+ [onChangeProp, setLocalValue]
76
+ );
64
77
  const renderFieldComponent = useCallback(
65
78
  ({ ariaDescribedBy, errorMessageElementId, id, labelElementId }) => (
66
79
  <MuiRadioGroup
@@ -72,12 +85,12 @@ const RadioGroup = ({
72
85
  id={id}
73
86
  name={nameOverride ?? id}
74
87
  onChange={onChange}
75
- value={value}
88
+ value={localValue}
76
89
  >
77
90
  {children}
78
91
  </MuiRadioGroup>
79
92
  ),
80
- [children, defaultValue, nameOverride, onChange, testId, value]
93
+ [children, defaultValue, nameOverride, onChange, testId, localValue]
81
94
  );
82
95
 
83
96
  return (
@@ -0,0 +1,56 @@
1
+ /*!
2
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
3
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
4
+ *
5
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ * Unless required by applicable law or agreed to in writing, software
7
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
8
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ *
10
+ * See the License for the specific language governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { useEffect, useRef, useState } from "react";
14
+
15
+ type UseControlledStateProps<Value> = {
16
+ controlledValue?: Value; // isChecked
17
+ uncontrolledValue?: Value; // isDefaultChecked
18
+ };
19
+
20
+ /**
21
+ * Use the same way as `useState`. Returns a stateful value, and a function to update it.
22
+ * When `initialState` is passed, the returned function to update it does nothing. This is
23
+ * useful to handle values in components that may be controlled externally when that value is
24
+ * passed in props and thus wish to prevent internal updates of the same value.
25
+ *
26
+ * @param initialState
27
+ * @see https://react.dev/reference/react/useState
28
+ */
29
+ export const useControlledState = <Value>({
30
+ controlledValue,
31
+ uncontrolledValue,
32
+ }: UseControlledStateProps<Value>) => {
33
+ const isControlledMode = useRef(controlledValue !== undefined);
34
+ const [stateValue, setStateValue] = useState(
35
+ isControlledMode.current ? controlledValue : uncontrolledValue
36
+ );
37
+
38
+ useEffect(() => {
39
+ if (isControlledMode.current) {
40
+ setStateValue(controlledValue);
41
+ }
42
+ }, [controlledValue]);
43
+
44
+ const setState: typeof setStateValue = (value) => {
45
+ if (!isControlledMode.current) {
46
+ setStateValue(value);
47
+ }
48
+ };
49
+
50
+ return [
51
+ stateValue,
52
+ // If `value` is controlled externally, ignore calls to the setter.
53
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
54
+ setState,
55
+ ] as const;
56
+ };