Skip to content

Conversation

rintumerin-aot
Copy link

@rintumerin-aot rintumerin-aot commented Sep 26, 2025

User description

Issue Tracking

JIRA: https://aottech.atlassian.net/browse/FWF-5209
Issue Type: FEATURE

Changes

Toggle/Switch component added

image image image image image image image

PR Type

Enhancement


Description

  • Add reusable Switch component

  • Export Switch from components package

  • Include switch SCSS styles and variables

  • Add switch icons to SvgIcons


Diagram Walkthrough

flowchart LR
  SwitchTSX["Switch.tsx component"] -- "uses" --> SvgIcons["SwitchTick/Cross icons"]
  SwitchTSX -- "styled by" --> SwitchSCSS["v8 _switch.scss"]
  ComponentsIndex["components index.ts"] -- "exports" --> SwitchTSX
  Declarations["declarations.d.ts (admin/review/submissions)"] -- "expose Switch" --> Apps["Host applications"]
  ThemeIndex["theme index.scss"] -- "imports" --> SwitchSCSS
Loading

File Walkthrough

Relevant files
Enhancement
8 files
declarations.d.ts
Expose Switch in admin components typing                                 
+2/-1     
Switch.tsx
New configurable Switch React component                                   
+127/-0 
index.tsx
Add switch tick/cross SVG icons                                                   
+14/-1   
index.ts
Export Switch from components package                                       
+3/-3     
declarations.d.ts
Expose Switch in review app typings                                           
+1/-0     
declarations.d.ts
Expose Switch in submissions app typings                                 
+1/-0     
index.scss
Import switch SCSS into theme bundle                                         
+1/-0     
_switch.scss
Add SCSS styles for custom switch                                               
+78/-0   

Copy link

github-actions bot commented Sep 26, 2025

PR Reviewer Guide 🔍

(Review updated until commit 71cb339)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Accessibility

The button sets id as ${id}-label and uses aria-labelledby referencing the label's id, but the visible label uses htmlFor={id}. This mismatch may break accessible labeling. Consider aligning ids (button id and label htmlFor) and using aria-labelledby consistently, or aria-label fallback logic when no label.

<div className={`custom-switch-wrapper ${className}`}>
  {label && (
    <label htmlFor={id} className="custom-switch-label">
      {label}
    </label>
  )}
  <button
    type="button"
    id={id ? `${id}-label` : undefined}
    ref={switchRef}
    role="switch"
    aria-checked={isChecked}
    aria-disabled={disabled}
    aria-labelledby={label && id ? `${id}-label` : undefined}
    aria-label={!label ? label ?? 'Toggle' : undefined}
    tabIndex={disabled ? -1 : 0}
Controlled vs Uncontrolled

isChecked state initializes from checked prop but does not sync when checked changes from parent. This can lead to stale UI when used as a controlled component. Consider supporting controlled mode (derive from prop with useEffect) or documenting it as uncontrolled with defaultChecked.

const [isChecked, setIsChecked] = useState(checked);
const [isFocused, setIsFocused] = useState(false);
const switchRef = useRef<HTMLButtonElement>(null);

// Use CSS variables for colors
const colorSuccess = StyleServices.getCSSVariable('--ff-success'); // for #00C49A
const colorPrimaryLight = StyleServices.getCSSVariable('--ff-primary-light'); // for #B8ABFF
const colorDanger = StyleServices.getCSSVariable('--ff-danger'); // for #E57373
SVG Props Handling

New SwitchTickIcon and SwitchCrossIcon ignore incoming props and hardcode width/height; also other icons added {...props} on path or svg inconsistently. For consistency and flexibility, spread {...props} on the svg element, and prefer using color/fill via props.

export const SwitchTickIcon = ({fillColor = baseColor, ...props}) => (
  <svg xmlns="http://www.w3.org/2000/svg" width="11" height="9" viewBox="0 0 11 9" fill="none">
    <path d="M3.8 8.01667L0 4.21667L0.95 3.26667L3.8 6.11667L9.91667 0L10.8667 0.95L3.8 8.01667Z" fill={fillColor}/>
  </svg>
);

export const SwitchCrossIcon = ({fillColor = baseColor, ...props}) => (
  <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" fill="none">
      <path d="M0.933333 9.33333L0 8.4L3.73333 4.66667L0 0.933333L0.933333 0L4.66667 3.73333L8.4 0L9.33333 0.933333L5.6 4.66667L9.33333 8.4L8.4 9.33333L4.66667 5.6L0.933333 9.33333Z" 
      fill={fillColor}/>
  </svg>
);

Copy link

github-actions bot commented Sep 26, 2025

PR Code Suggestions ✨

Latest suggestions up to 71cb339

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix label-to-control association

The association between the visible label and the switch control is incorrect. The
label's htmlFor points to id, but the button sets id to ${id}-label and uses
aria-labelledby, which breaks accessibility tools. Assign the button the provided
id, and reference that in the label and ARIA attributes.

forms-flow-components/src/components/CustomComponents/Switch.tsx [104-121]

 <button
   type="button"
-  id={id ? `${id}-label` : undefined}
+  id={id}
   ref={switchRef}
   role="switch"
   aria-checked={isChecked}
   aria-disabled={disabled}
-  aria-labelledby={label && id ? `${id}-label` : undefined}
+  aria-labelledby={label && id ? id : undefined}
   aria-label={!label ? label ?? 'Toggle' : undefined}
   tabIndex={disabled ? -1 : 0}
   className={renderClass()}
   onClick={handleToggle}
   onKeyDown={handleKeyDown}
   onFocus={handleFocus}
   onBlur={handleBlur}
   disabled={disabled}
 >
Suggestion importance[1-10]: 8

__

Why: The current label uses htmlFor=id while the button's id is ${id}-label, breaking association; the fix correctly aligns id and aria-labelledby, improving accessibility and correctness.

Medium
Sync state with prop changes

The component does not update when the external checked prop changes after mount,
causing state desynchronization. Sync internal state with prop changes using an
effect to reflect controlled updates.

forms-flow-components/src/components/CustomComponents/Switch.tsx [26-27]

 const [isChecked, setIsChecked] = useState(checked);
+React.useEffect(() => {
+  setIsChecked(checked);
+}, [checked]);
Suggestion importance[1-10]: 7

__

Why: Without syncing, the internal isChecked won't reflect external checked updates; adding an effect is accurate and improves controlled behavior, though not a critical bug in all usages.

Medium
General
Ensure non-empty aria-label fallback

When label is falsy, aria-label may receive undefined, harming screen reader
usability. Always provide a non-empty fallback string to ensure the switch is
announced properly.

forms-flow-components/src/components/CustomComponents/Switch.tsx [112]

-aria-label={!label ? label ?? 'Toggle' : undefined}
+aria-label={label ? undefined : 'Toggle'}
Suggestion importance[1-10]: 6

__

Why: The current expression can pass an empty string or undefined; the change ensures a consistent, non-empty aria-label when no visible label exists, improving accessibility.

Low

Previous suggestions

Suggestions up to commit 9ba6594
CategorySuggestion                                                                                                                                    Impact
Possible issue
Sync state with prop changes

Sync internal state when the controlled prop changes. Without this, external updates
to checked won't reflect in the UI, causing stale state.

forms-flow-components/src/components/CustomComponents/Switch.tsx [26-27]

 const [isChecked, setIsChecked] = useState(checked);
+React.useEffect(() => {
+  setIsChecked(checked);
+}, [checked]);
Suggestion importance[1-10]: 8

__

Why: Without syncing isChecked when checked prop changes, the component can get out of sync in controlled scenarios. Adding an effect to mirror prop updates is correct and impactful for correctness.

Medium
Correct ARIA associations

Fix ARIA labelling: the button's id should be the control's id and the visible
label's id should be referenced via aria-labelledby. Also, only set aria-label when
there is no visible label text.

forms-flow-components/src/components/CustomComponents/Switch.tsx [104-124]

+<label id={id ? `${id}-label` : undefined} htmlFor={id} className="custom-switch-label">
+  {label}
+</label>
 <button
   type="button"
-  id={id ? `${id}-label` : undefined}
+  id={id}
   ref={switchRef}
   role="switch"
   aria-checked={isChecked}
   aria-disabled={disabled}
-  aria-labelledby={label && id ? `${id}-label` : undefined}
+  aria-labelledby={label ? `${id}-label` : undefined}
   aria-label={!label ? label ?? 'Toggle' : undefined}
   tabIndex={disabled ? -1 : 0}
   className={renderClass()}
   onClick={handleToggle}
   onKeyDown={handleKeyDown}
   onFocus={handleFocus}
   onBlur={handleBlur}
   disabled={disabled}
 >
Suggestion importance[1-10]: 7

__

Why: The ARIA association is slightly misapplied: the button currently sets its own id to ${id}-label and references aria-labelledby to that; swapping so the visible label has ${id}-label and the control uses id with aria-labelledby is a valid accessibility improvement. It aligns with ARIA practices and is accurate to the code context, though not a critical bug.

Medium
General
Ensure non-empty accessible name

Ensure a stable fallback accessible name when no label is provided. Using label ??
'Toggle' can yield an empty string; default to a non-empty string.

forms-flow-components/src/components/CustomComponents/Switch.tsx [112]

-aria-label={!label ? label ?? 'Toggle' : undefined}
+aria-label={label && label.trim().length > 0 ? undefined : 'Toggle'}
Suggestion importance[1-10]: 6

__

Why: The current aria-label={!label ? label ?? 'Toggle' : undefined} can pass an empty string, harming accessibility. The proposed guard ensures a non-empty fallback; it's a reasonable, minor improvement.

Low
Suggestions up to commit 80d49a9
CategorySuggestion                                                                                                                                    Impact
Possible issue
Correct ARIA labeling wiring

Fix miswired ARIA labelling to ensure assistive technologies read the correct label.
The button should use id={id} (not ${id}-label), and aria-labelledby should point to
the visible text element's id (e.g., ${id}-label) while the should associate with
the control via htmlFor={id}. Also only set one of aria-labelledby or aria-label.

forms-flow-components/src/components/CustomComponents/Switch.tsx [104-124]

+{label && (
+  <label htmlFor={id} className="custom-switch-label" id={id ? `${id}-label` : undefined}>
+    {label}
+  </label>
+)}
 <button
   type="button"
-  id={id ? `${id}-label` : undefined}
+  id={id}
   ref={switchRef}
   role="switch"
   aria-checked={isChecked}
   aria-disabled={disabled}
   aria-labelledby={label && id ? `${id}-label` : undefined}
-  aria-label={!label ? label ?? 'Toggle' : undefined}
+  aria-label={!label ? (label ?? 'Toggle') : undefined}
   tabIndex={disabled ? -1 : 0}
   className={renderClass()}
   onClick={handleToggle}
   onKeyDown={handleKeyDown}
   onFocus={handleFocus}
   onBlur={handleBlur}
   disabled={disabled}
 >
Suggestion importance[1-10]: 8

__

Why: The current ARIA labelling misassigns the id to ${id}-label on the control and uses aria-labelledby without giving the label element an id; the proposed fix correctly wires htmlFor, control id, and aria-labelledby, improving accessibility without altering behavior.

Medium
Sync state with prop changes

Keep internal state in sync when the controlled checked prop changes. Without this,
external updates to checked will not reflect in the UI, causing stale state and
broken controlled usage.

forms-flow-components/src/components/CustomComponents/Switch.tsx [26-27]

 const [isChecked, setIsChecked] = useState(checked);
+React.useEffect(() => {
+  setIsChecked(checked);
+}, [checked]);
Suggestion importance[1-10]: 7

__

Why: Adds an effect to keep isChecked in sync with the checked prop, enabling controlled usage and preventing stale UI; low-risk and directly relevant.

Medium
General
Provide color fallbacks

Add fallbacks if CSS variables are unavailable to prevent runtime null/undefined
colors that break icon rendering. Default to the hardcoded hex values indicated in
comments when CSS variables resolve to falsy values.

forms-flow-components/src/components/CustomComponents/Switch.tsx [31-34]

-const colorSuccess = StyleServices.getCSSVariable('--ff-success'); // for #00C49A
-const colorPrimaryLight = StyleServices.getCSSVariable('--ff-primary-light'); // for #B8ABFF
-const colorDanger = StyleServices.getCSSVariable('--ff-danger'); // for #E57373
-const colorGrayLight = StyleServices.getCSSVariable('--ff-gray-light'); // for #E5E5E5
+const colorSuccess = StyleServices.getCSSVariable('--ff-success') || '#00C49A';
+const colorPrimaryLight = StyleServices.getCSSVariable('--ff-primary-light') || '#B8ABFF';
+const colorDanger = StyleServices.getCSSVariable('--ff-danger') || '#E57373';
+const colorGrayLight = StyleServices.getCSSVariable('--ff-gray-light') || '#E5E5E5';
Suggestion importance[1-10]: 6

__

Why: Supplying hex fallbacks for CSS variables prevents undefined colors breaking icon rendering; beneficial but not critical if variables are always defined.

Low
Suggestions up to commit 982d7df
CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix erroneous assignment in condition

Replace the assignment operator in the condition with a strict equality check to
avoid mutating type and causing incorrect rendering. This bug will set type to
'primary' every time isChecked is true.

forms-flow-components/src/components/CustomComponents/Switch.tsx [33-35]

 if (isChecked) {
-    if(type= 'primary') fillColor ='#B8ABFF';
+    if (type === 'primary') fillColor = '#B8ABFF';
Suggestion importance[1-10]: 9

__

Why: The code uses assignment (type = 'primary') inside a conditional, which is a bug that mutates type and breaks logic; replacing it with strict equality is correct and important for proper rendering.

High
General
Correct label-control association

Ensure the visual label is programmatically linked to the control by assigning an id
to the label and referencing it via aria-labelledby. Using htmlFor with a button
does not create an accessible relationship.

forms-flow-components/src/components/CustomComponents/Switch.tsx [92-101]

 {label && (
-  <label htmlFor={id} className="custom-switch-label">
+  <label id={id ? `${id}-label` : undefined} className="custom-switch-label">
     {label}
   </label>
 )}
 <button
   type="button"
   id={id}
   ref={switchRef}
   role="switch"
Suggestion importance[1-10]: 7

__

Why: Using an id on the label and referencing it via aria-labelledby creates a proper programmatic association since htmlFor does not link to a button; this is accurate and improves accessibility.

Medium
Improve switch ARIA labeling

Add aria-label or associate the visible label with the control using aria-labelledby
to ensure screen readers announce the switch purpose. Also set aria-readonly instead
of aria-disabled when using a disabled button role="switch" for better semantics,
and ensure id/label linkage when label is provided.

forms-flow-components/src/components/CustomComponents/Switch.tsx [102-106]

 aria-checked={isChecked}
 aria-disabled={disabled}
+aria-labelledby={label && id ? `${id}-label` : undefined}
+aria-label={!label ? label ?? 'Toggle' : undefined}
 tabIndex={disabled ? -1 : 0}
 className={renderClass()}
Suggestion importance[1-10]: 6

__

Why: Enhancing ARIA labeling via aria-labelledby/aria-label improves accessibility; however, it retains aria-disabled and mixes concerns partially, so impact is moderate.

Low

}else{
if(type == 'binary')fillColor = '#E57373';
else fillColor = '#E5E5E5';

Copy link
Contributor

@fahad-aot fahad-aot Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of hashcode, we can use variable here using a style service like const disabledPrimaryColor = StyleServices.getCSSVariable('--primary'); @rintumerin-aot

Copy link

Persistent review updated to latest commit 80d49a9

Copy link

Copy link

Persistent review updated to latest commit 9ba6594

@arun-s-aot arun-s-aot merged commit 8c8da26 into AOT-Technologies:v8-develop Sep 30, 2025
4 checks passed
Copy link

Persistent review updated to latest commit 71cb339

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants