From 5f02f7961ec198bba2542670ddc68a4634d96e6f Mon Sep 17 00:00:00 2001 From: Zeeshan Mehboob Date: Mon, 27 Jul 2026 09:33:31 +0530 Subject: [PATCH 1/5] [MODIFIED] consent component, added tooltip Signed-off-by: Zeeshan Mehboob --- .gitignore | 4 + .../react/src/components/adapters/Consent.tsx | 91 ++++++++++-- .../adapters/ConsentCheckboxList.styles.ts | 1 + .../adapters/ConsentCheckboxList.tsx | 31 ++-- .../presentation/auth/AuthOptionFactory.tsx | 1 + .../primitives/Tooltip/Tooltip.styles.ts | 136 ++++++++++++++++++ .../components/primitives/Tooltip/Tooltip.tsx | 79 ++++++++++ 7 files changed, 319 insertions(+), 24 deletions(-) create mode 100644 packages/react/src/components/primitives/Tooltip/Tooltip.styles.ts create mode 100644 packages/react/src/components/primitives/Tooltip/Tooltip.tsx diff --git a/.gitignore b/.gitignore index 005cdd1..eb74eca 100644 --- a/.gitignore +++ b/.gitignore @@ -149,3 +149,7 @@ Thumbs.db # Turbo .turbo + +.nx +__tests__ +.vitest-attachments diff --git a/packages/react/src/components/adapters/Consent.tsx b/packages/react/src/components/adapters/Consent.tsx index 42bf87f..cee308d 100644 --- a/packages/react/src/components/adapters/Consent.tsx +++ b/packages/react/src/components/adapters/Consent.tsx @@ -17,9 +17,13 @@ */ import {type ConsentPurposeData} from '@thunderid/browser'; -import {FC, ReactNode} from 'react'; -import ConsentCheckboxList from './ConsentCheckboxList'; +import {type ChangeEvent, FC, ReactNode} from 'react'; +import ConsentCheckboxList, {getConsentOptionalKey} from './ConsentCheckboxList'; import Typography from '../primitives/Typography/Typography'; +import Toggle from '../primitives/Toggle/Toggle'; +import {PromptElement} from '../../../../javascript/dist/models/embedded-flow'; +import {Info} from '../primitives/Icons'; +import Tooltip from '../primitives/Tooltip/Tooltip'; /** * Backward-compatible consent purpose type exported by @thunderid/react. @@ -73,14 +77,54 @@ export interface ConsentProps { * Callback invoked when a user toggles an optional attribute. */ onInputChange: (name: string, value: string) => void; + /** + * Config to modified detail in consent page + */ + config?: Record; } +const defaultConfig = { + essential: 'Essential Attributtes', + optional: 'Optional Attributes', +}; + /** * Consent component renders the list of purposes and their associated attributes (essential and optional) * based on the data provided by the backend. It allows users to toggle optional attributes while essential * attributes are displayed as read-only. */ -const Consent: FC = ({consentData, formValues, onInputChange, children}: ConsentProps) => { +const Consent: FC = ({ + consentData, + formValues, + config = defaultConfig, + onInputChange, + children, +}: ConsentProps) => { + /** + * Method to check whether master toggle button is checked or not + * @param purpose Purpose object + * @param checked boolean variable to check, whether toggle is checked or unchecked + */ + const handleChange = (purpose: ConsentPurposeData, checked: boolean): void => { + const checkValue = checked ? 'true' : 'false'; + purpose.optional.map((opt: PromptElement) => { + const key: string = getConsentOptionalKey(purpose.purposeId, opt.name); + onInputChange(key, checkValue); + }); + }; + + /** + * Check all optional claims are selected or not + * @param purpose Purpose object + * @returns boolean value to denote all optional claims are selected + */ + const checkOptValue = (purpose: ConsentPurposeData): boolean => { + return purpose.optional.every((opt: PromptElement) => { + const key: string = getConsentOptionalKey(purpose.purposeId, opt.name); + return formValues[key] === 'true'; + }); + }; + if (!consentData) return null; let purposes: ConsentPurposeData[] = []; @@ -115,9 +159,16 @@ const Consent: FC = ({consentData, formValues, onInputChange, chil {purpose.essential && purpose.essential.length > 0 && (
- - Essential Attributes - +
+ + {config['essential'] as string} + + {(config['essentialInfo'] as string) !== '' && ( + + + + )} +
= ({consentData, formValues, onInputChange, chil {purpose.optional && purpose.optional.length > 0 && (
- - {purpose.type === 'permissions' ? 'Permissions' : 'Optional Attributes'} - +
+
+ + {purpose.type === 'permissions' ? 'Permissions' : (config['optional'] as string)} + + {(config['optionalInfo'] as string) !== '' && ( + + + + )} +
+ ): void => handleChange(purpose, e.target.checked)} + /> +
=> `, listItem: css` padding: 0 0.25rem; + margin-bottom: 4px; `, listRow: css` display: flex; diff --git a/packages/react/src/components/adapters/ConsentCheckboxList.tsx b/packages/react/src/components/adapters/ConsentCheckboxList.tsx index 815506a..1c26a40 100644 --- a/packages/react/src/components/adapters/ConsentCheckboxList.tsx +++ b/packages/react/src/components/adapters/ConsentCheckboxList.tsx @@ -128,8 +128,8 @@ const ConsentCheckboxList: FC = ({ 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'; + // Default to be false when there's no explicit form value + return formValues[key] === 'true'; }; const handleChange = (attrName: string, checked: boolean): void => { @@ -172,20 +172,21 @@ const ConsentCheckboxList: FC = ({ {attr}
- ): void => handleChange(attr, e.target.checked) - } - /> + {isEssential ? ( + Required + ) : ( + ): void => handleChange(attr, e.target.checked) + } + /> + )}
- ); })} diff --git a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx index eb1d1c5..03479ed 100644 --- a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx +++ b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx @@ -772,6 +772,7 @@ const createAuthComponentFromFlow = ( consentData={consentPromptRawData as any} formValues={formValues} onInputChange={onInputChange} + config={component.config} /> ); } diff --git a/packages/react/src/components/primitives/Tooltip/Tooltip.styles.ts b/packages/react/src/components/primitives/Tooltip/Tooltip.styles.ts new file mode 100644 index 0000000..3c3f6ba --- /dev/null +++ b/packages/react/src/components/primitives/Tooltip/Tooltip.styles.ts @@ -0,0 +1,136 @@ +/** + * 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 {css, keyframes} from '@emotion/css'; +import {Theme} from '@thunderid/browser'; +import {useMemo} from 'react'; + +const useStyles = (theme: Theme, colorScheme: string): Record => + useMemo(() => { + const fadeInAnimation = keyframes` + from { opacity: 0; } + to { opacity: 1; } + `; + + const containerStyles: string = css` + position: relative; + display: inline-block; + `; + + // white-space: nowrap; + const boxStyles: string = css` + position: absolute; + background-color: #333; + color: #fff; + padding: 6px 12px; + border-radius: 6px; + font-size: 0.85rem; + z-index: 1000; + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.15); + animation: ${fadeInAnimation} 0.15s ease-in-out; + + min-width: 250px; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + text-align: start; + `; + + const topStyles: string = css` + bottom: 100%; + left: 50%; + transform: translateX(-50%); + margin-bottom: 8px; + `; + + const bottomStyles: string = css` + top: 100%; + left: 50%; + transform: translateX(-50%); + margin-top: 8px; + `; + + const leftStyles: string = css` + right: 100%; + top: 50%; + transform: translateY(-50%); + margin-right: 8px; + `; + + const rightStyles: string = css` + left: 100%; + top: 50%; + transform: translateY(-50%); + margin-left: 8px; + `; + + const arrowStyles: string = css` + position: absolute; + width: 0; + height: 0; + border-style: solid; + `; + + const topArrowStyles: string = css` + top: 100%; + left: 50%; + transform: translateX(-50%); + border-width: 5px 5px 0 5px; + border-color: #333 transparent transparent transparent; + `; + + const bottomArrowStyles: string = css` + bottom: 100%; + left: 50%; + transform: translateX(-50%); + border-width: 0 5px 5px 5px; + border-color: transparent transparent #333 transparent; + `; + + const leftArrowStyles: string = css` + left: 100%; + top: 50%; + transform: translateY(-50%); + border-width: 5px 0 5px 5px; + border-color: transparent transparent transparent #333; + `; + + const rightArrowStyles: string = css` + right: 100%; + top: 50%; + transform: translateY(-50%); + border-width: 5px 5px 5px 0; + border-color: transparent #333 transparent transparent; + `; + + return { + container: containerStyles, + box: boxStyles, + top: topStyles, + bottom: bottomStyles, + left: leftStyles, + right: rightStyles, + arrow: arrowStyles, + 'top-arrow': topArrowStyles, + 'bottom-arrow': bottomArrowStyles, + 'left-arrow': leftArrowStyles, + 'right-arrow': rightArrowStyles, + }; + }, [theme, colorScheme]); + +export default useStyles; diff --git a/packages/react/src/components/primitives/Tooltip/Tooltip.tsx b/packages/react/src/components/primitives/Tooltip/Tooltip.tsx new file mode 100644 index 0000000..111ac18 --- /dev/null +++ b/packages/react/src/components/primitives/Tooltip/Tooltip.tsx @@ -0,0 +1,79 @@ +/** + * 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 {cx} from '@emotion/css'; +import {withVendorCSSClassPrefix, bem} from '@thunderid/browser'; +import {FC, InputHTMLAttributes, ReactNode, useState} from 'react'; +import useStyles from './Tooltip.styles'; +import useTheme from '../../../contexts/Theme/useTheme'; + +/** + * Props for the Tooltip component. + */ +export interface Tooltiprops extends Omit, 'className' | 'type'> { + className?: string; + helperText?: string; + position?: 'top' | 'bottom' | 'left' | 'right'; + children?: ReactNode; +} + +/** + * A Tooltip component that convert any react or html element to a tooltip. + * When a user hover on it, they will see a small tooltip with some info. + * + * It will take a children, which can be a react element or html element. + * + * @param props - Props for the Tooltip component + * @returns A JSX element representing the Tooltip + */ +const Tooltip: FC = ({className, helperText, style = {}, position, children, ...rest}: Tooltiprops) => { + const {theme, colorScheme}: ReturnType = useTheme(); + const styles: Record = useStyles(theme, colorScheme); + const [isVisible, setIsVisible] = useState(false); + + return ( +
setIsVisible(true)} + onMouseLeave={() => setIsVisible(false)} + > + {children} + {isVisible && ( +
+ {helperText} + +
+ )} +
+ ); +}; + +export default Tooltip; From f53c77911e0589b757149bea7bfb97703f2bfabf Mon Sep 17 00:00:00 2001 From: Zeeshan Mehboob Date: Mon, 27 Jul 2026 14:13:43 +0530 Subject: [PATCH 2/5] [4279] resolved coderabbit comment Signed-off-by: Zeeshan Mehboob --- packages/javascript/src/index.ts | 1 + .../react/src/components/adapters/Consent.tsx | 29 +++++++++---- .../adapters/ConsentCheckboxList.tsx | 1 - .../presentation/auth/AuthOptionFactory.tsx | 2 +- .../components/primitives/Tooltip/Tooltip.tsx | 41 +++++++++++++++++-- .../auth/sign-in/AuthOptionFactoryCore.ts | 2 +- 6 files changed, 62 insertions(+), 14 deletions(-) diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index cae4152..e5d0b5d 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -61,6 +61,7 @@ export type { EmbeddedFlowExecuteRequestConfig, FlowExecutionError, ConsentAttributeElement, + PromptElement, ConsentPurposeDecision, ConsentDecisions, ConsentPurposeData, diff --git a/packages/react/src/components/adapters/Consent.tsx b/packages/react/src/components/adapters/Consent.tsx index cee308d..625c71d 100644 --- a/packages/react/src/components/adapters/Consent.tsx +++ b/packages/react/src/components/adapters/Consent.tsx @@ -16,12 +16,11 @@ * under the License. */ -import {type ConsentPurposeData} from '@thunderid/browser'; +import {type ConsentPurposeData, PromptElement} from '@thunderid/browser'; import {type ChangeEvent, FC, ReactNode} from 'react'; import ConsentCheckboxList, {getConsentOptionalKey} from './ConsentCheckboxList'; import Typography from '../primitives/Typography/Typography'; import Toggle from '../primitives/Toggle/Toggle'; -import {PromptElement} from '../../../../javascript/dist/models/embedded-flow'; import {Info} from '../primitives/Icons'; import Tooltip from '../primitives/Tooltip/Tooltip'; @@ -44,6 +43,16 @@ export interface ConsentRenderProps { purposes: ConsentPurposeData[]; } +/** + * Interface for consent configuration + */ +export interface ConsentConfig { + essential?: string; + optional?: string; + essentialInfo?: string; + optionalInfo?: string; +} + /** * Props for the Consent component. */ @@ -83,7 +92,7 @@ export interface ConsentProps { config?: Record; } -const defaultConfig = { +const defaultConfig: Required> = { essential: 'Essential Attributtes', optional: 'Optional Attributes', }; @@ -96,10 +105,13 @@ const defaultConfig = { const Consent: FC = ({ consentData, formValues, - config = defaultConfig, + config: suppliedConfig, onInputChange, children, }: ConsentProps) => { + const config: ConsentConfig = {...defaultConfig, ...suppliedConfig}; + const essentialInfo = typeof config.essentialInfo === 'string' ? config.essentialInfo.trim() : ''; + const optionalInfo = typeof config.optionalInfo === 'string' ? config.optionalInfo.trim() : ''; /** * Method to check whether master toggle button is checked or not * @param purpose Purpose object @@ -163,8 +175,8 @@ const Consent: FC = ({ {config['essential'] as string} - {(config['essentialInfo'] as string) !== '' && ( - + {essentialInfo !== '' && ( + )} @@ -193,8 +205,8 @@ const Consent: FC = ({ {purpose.type === 'permissions' ? 'Permissions' : (config['optional'] as string)} - {(config['optionalInfo'] as string) !== '' && ( - + {optionalInfo !== '' && ( + )} @@ -202,6 +214,7 @@ const Consent: FC = ({ ): void => handleChange(purpose, e.target.checked)} /> diff --git a/packages/react/src/components/adapters/ConsentCheckboxList.tsx b/packages/react/src/components/adapters/ConsentCheckboxList.tsx index 1c26a40..6d60c11 100644 --- a/packages/react/src/components/adapters/ConsentCheckboxList.tsx +++ b/packages/react/src/components/adapters/ConsentCheckboxList.tsx @@ -21,7 +21,6 @@ import {type ConsentPurposeData, withVendorCSSClassPrefix, bem} from '@thunderid 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'; diff --git a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx index 03479ed..2247547 100644 --- a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx +++ b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx @@ -394,7 +394,7 @@ const createAuthComponentFromFlow = ( ...p.essential.map((e): ConsentAttributeElement => ({approved: !isDeny, name: e.name})), ...p.optional.map( (e): ConsentAttributeElement => ({ - approved: isDeny ? false : formValues[getConsentOptionalKey(p.purposeId, e.name)] !== 'false', + approved: !isDeny && formValues[getConsentOptionalKey(p.purposeId, e.name)] === 'true', name: e.name, }), ), diff --git a/packages/react/src/components/primitives/Tooltip/Tooltip.tsx b/packages/react/src/components/primitives/Tooltip/Tooltip.tsx index 111ac18..61e41cc 100644 --- a/packages/react/src/components/primitives/Tooltip/Tooltip.tsx +++ b/packages/react/src/components/primitives/Tooltip/Tooltip.tsx @@ -18,7 +18,7 @@ import {cx} from '@emotion/css'; import {withVendorCSSClassPrefix, bem} from '@thunderid/browser'; -import {FC, InputHTMLAttributes, ReactNode, useState} from 'react'; +import {FC, InputHTMLAttributes, ReactNode, useId, useState, KeyboardEvent} from 'react'; import useStyles from './Tooltip.styles'; import useTheme from '../../../contexts/Theme/useTheme'; @@ -30,6 +30,7 @@ export interface Tooltiprops extends Omit, helperText?: string; position?: 'top' | 'bottom' | 'left' | 'right'; children?: ReactNode; + ariaLabel?: string; } /** @@ -41,20 +42,54 @@ export interface Tooltiprops extends Omit, * @param props - Props for the Tooltip component * @returns A JSX element representing the Tooltip */ -const Tooltip: FC = ({className, helperText, style = {}, position, children, ...rest}: Tooltiprops) => { +const Tooltip: FC = ({ + className, + helperText, + style = {}, + position = 'bottom', + children, + ariaLabel = 'More Info', + ...rest +}: Tooltiprops) => { const {theme, colorScheme}: ReturnType = useTheme(); const styles: Record = useStyles(theme, colorScheme); const [isVisible, setIsVisible] = useState(false); + // Unique ID to connect trigger button to tooltip box via ARIA + const tooltipId = useId(); + + // Close tooltip if user presses ESC while focused + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isVisible) { + setIsVisible(false); + } + + // Optional: Prevent Space key from scrolling the page + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + setIsVisible((prev) => !prev); + } + }; return (
setIsVisible(true)} onMouseLeave={() => setIsVisible(false)} + onFocus={() => setIsVisible(true)} /* Trigger on Tab Focus */ + onBlur={() => setIsVisible(false)} /* Hide on Tab Blur */ + onKeyDown={handleKeyDown} /* Allow ESC to dismiss */ + {...rest} > {children} - {isVisible && ( + {isVisible && helperText && (