-
Notifications
You must be signed in to change notification settings - Fork 60
Expand file tree
/
Copy pathConsentCheckboxList.tsx
More file actions
194 lines (179 loc) · 6.85 KB
/
ConsentCheckboxList.tsx
File metadata and controls
194 lines (179 loc) · 6.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {type ConsentPurposeDataV2 as ConsentPurposeData, withVendorCSSClassPrefix, bem} from '@asgardeo/browser';
import {cx} from '@emotion/css';
import {type ChangeEvent, FC, ReactNode} from 'react';
import useStyles from './ConsentCheckboxList.styles';
import useTheme from '../../contexts/Theme/useTheme';
import Divider from '../primitives/Divider/Divider';
import Toggle from '../primitives/Toggle/Toggle';
import Typography from '../primitives/Typography/Typography';
/**
* Computes the form value key for tracking an optional attribute's consent state.
*
* @param purposeId - The ID of the consent purpose.
* @param attrName - The name of the attribute.
* @returns A stable form key string.
*/
export const getConsentOptionalKey = (purposeId: string, attrName: string): string =>
`__consent_opt__${purposeId}__${attrName}`;
/**
* ConsentInputVariant defines whether the ConsentCheckboxList is rendering essential (read-only) or
* optional (toggleable) attributes.
*/
export type ConsentInputVariant = 'ESSENTIAL' | 'OPTIONAL';
/**
* Render props exposed by ConsentCheckboxList when using the render-prop pattern.
*/
export interface ConsentCheckboxListRenderProps {
/** The list of attribute names to render. */
attributes: string[];
/**
* Call this when an optional attribute checkbox is toggled.
* No-op for ESSENTIAL attributes (they cannot be changed).
*/
handleChange: (attrName: string, checked: boolean) => void;
/**
* Returns the current checked state for the given attribute name.
* Always returns true for ESSENTIAL attributes.
*/
isChecked: (attrName: string) => boolean;
/** Whether the list is rendering essential or optional attributes. */
variant: ConsentInputVariant;
}
/**
* Props for the ConsentCheckboxList component.
*/
export interface ConsentCheckboxListProps {
/**
* Render-props callback. When provided, the default checkbox list UI is replaced
* with whatever JSX the callback returns.
*
* @example
* ```tsx
* <ConsentCheckboxList variant="OPTIONAL" purpose={purpose} formValues={formValues} onInputChange={onChange}>
* {({ attributes, isChecked, handleChange }) => (
* <ul>
* {attributes.map(attr => (
* <li key={attr}>
* <Checkbox checked={isChecked(attr)} onChange={e => handleChange(attr, e.target.checked)} />
* {attr}
* </li>
* ))}
* </ul>
* )}
* </ConsentCheckboxList>
* ```
*/
children?: (props: ConsentCheckboxListRenderProps) => ReactNode;
/** Current form values - used to read optional checkbox state */
formValues: Record<string, string>;
/** Callback invoked when an optional attribute checkbox is toggled */
onInputChange: (name: string, value: string) => void;
/** The consent purpose data containing attribute lists */
purpose: ConsentPurposeData;
/** Whether to render essential (disabled) or optional (toggleable) attributes */
variant: ConsentInputVariant;
}
/**
* Renders a list of consent attribute checkboxes.
*
* - ESSENTIAL variant: renders read-only checked checkboxes for required attributes.
* - OPTIONAL variant: renders toggleable checkboxes for optional attributes.
* Opt-in is the default when no prior form value exists.
*/
const ConsentCheckboxList: FC<ConsentCheckboxListProps> = ({
variant,
purpose,
formValues,
onInputChange,
children,
}: ConsentCheckboxListProps) => {
const {theme, colorScheme}: ReturnType<typeof useTheme> = useTheme();
const styles: Record<string, string> = useStyles(theme, colorScheme);
const attributes: string[] = variant === 'ESSENTIAL' ? purpose.essential : purpose.optional;
if (!attributes || attributes.length === 0) {
return null;
}
const isEssential: boolean = variant === 'ESSENTIAL';
const isChecked = (attrName: string): boolean => {
if (isEssential) {
return true;
}
const key: string = getConsentOptionalKey(purpose.purposeId, attrName);
// Default to opted-in (true) when there's no explicit form value
return formValues[key] !== 'false';
};
const handleChange = (attrName: string, checked: boolean): void => {
const key: string = getConsentOptionalKey(purpose.purposeId, attrName);
onInputChange(key, checked ? 'true' : 'false');
};
if (children) {
return <>{children({attributes, handleChange, isChecked, variant})}</>;
}
return (
<div className={cx(withVendorCSSClassPrefix(bem('consent-checkbox-list')), styles['listContainer'])}>
{attributes.map((attr: string) => {
const inputId: string = `consent_${isEssential ? 'ess' : 'opt'}_${purpose.purposeId}_${attr}`;
const checked: boolean = isChecked(attr);
return (
<div
key={attr}
className={cx(withVendorCSSClassPrefix(bem('consent-checkbox-list', 'item')), styles['listItem'])}
>
<div className={cx(withVendorCSSClassPrefix(bem('consent-checkbox-list', 'row')), styles['listRow'])}>
<div
className={cx(
withVendorCSSClassPrefix(bem('consent-checkbox-list', 'label-container')),
styles['labelContainer'],
)}
>
<div
className={cx(withVendorCSSClassPrefix(bem('consent-checkbox-list', 'bullet')), styles['bullet'])}
/>
<Typography
variant="body2"
className={cx(
withVendorCSSClassPrefix(bem('consent-checkbox-list', 'typography')),
styles['typography'],
)}
>
{attr}
</Typography>
</div>
<Toggle
id={inputId}
checked={checked}
disabled={isEssential}
onChange={
isEssential
? undefined
: (e: ChangeEvent<HTMLInputElement>): void => handleChange(attr, e.target.checked)
}
/>
</div>
<Divider
className={cx(withVendorCSSClassPrefix(bem('consent-checkbox-list', 'divider')), styles['divider'])}
/>
</div>
);
})}
</div>
);
};
export default ConsentCheckboxList;