Skip to content

Commit

Permalink
feat: add visual status indicators to the UIToggle component (#2698)
Browse files Browse the repository at this point in the history
* feat(ui-components): replace toggle thumb with custom icon

Add functionality to render a custom "SwitchOn" icon in the UIToggle's thumb element using ReactDOM. Updated styles and component structure to support this change and ensure compatibility.

* feat(ui-toggle): add icon support for toggle states

Introduce on/off icons for the UIToggle component. Updated logic to dynamically render the correct icon based on toggle state and included new SCSS for styling.

* feat: add customizable colors for switch icons

Introduce theme-based colors for 'SwitchOn' and 'SwitchOff' icons using `COLORS.thumbOn` and `COLORS.thumbOff`. This allows the icons to adapt to the current theme, improving UI consistency and flexibility.

* refactor: remove UIToggle.scss and inline styles

This removes the UIToggle.scss file and replaces its styles with inline styles in the component logic. The change simplifies style handling by consolidating them into the TypeScript file, reducing dependencies and ensuring better control over dynamic styling.

* fix(ui-toggle): standardize default styles for UIToggle

Updated UIToggle to include consistent default styles for the 'Standard' size, including dimensions and padding. Updated related tests to reflect these changes and ensure correctness. Added JSDoc comments to improve method documentation.

* refactor(ui-toggle): make size info properties optional

Updated UIToggleSizeInfo properties to be optional for flexibility. Improved documentation by adding JSDoc comments for key methods. Removed a leftover console.log statement for cleaner code.

* feat: add new SVG icons and enhance UIToggle component

Introduced SwitchOff and SwitchOn SVG icons. Updated the UIToggle component to include visual indicators for switch status, improving the user interface clarity.

* Linting auto fix commit

* refactor: remove unused icon definitions in Icons.tsx

Removed over 2000 lines of unused or outdated icon definitions from the Icons.tsx file to improve maintainability and reduce code bloat. These icons were no longer being used across the project, and their removal cleans up the codebase.

* feat(ui-toggle): update styles for hover and focus states

Refined hover and focus styles for UIToggle component, improving visual consistency by aligning with updated design tokens. Updated color variables and positioning, introduced cleaner styles for checked/unchecked states, and unified border behavior.

* fix(ui-toggle): improve hover styles and theme handling

Refined hover styles for both checked and unchecked toggle states, ensuring proper application of colors and borders. Added fallback to `contrast` variables for better theme compatibility. Removed redundant localStorage theme settings in stories.

* fix: update UIToggle styles for consistency and clarity

Adjusted UIToggle component styles to improve visual consistency, aligning with updated design tokens. Modified border widths, hover, and disabled states for both toggles and thumbs, ensuring they match the latest design guidelines.

* fix(ui-toggle): adjust padding and border color handling

Update padding values and improve border color logic for better appearance and consistency in the UIToggle component. Fix issues with incorrect border color assignments for both checked and unchecked states.

* fix(ui-toggle): adjust padding and border color styles

Updated `innerPadding` to `0 1px` and refined border color handling to include fallback `transparent`. These changes ensure consistent rendering and improve visual clarity in various states.

* fix: update UIToggle to use UiIcons constants

Replaced hardcoded icon names with UiIcons constants in UIToggle. This ensures consistency and easier maintainability for icon usage across the UI components.

* refactor: add readonly to toggleRootRef in UIToggle

Marked toggleRootRef as readonly to prevent unintended reassignments. This ensures immutability and improves the reliability of the component.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
1 parent 5319904 commit 0f9d186
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 74 deletions.
6 changes: 6 additions & 0 deletions .changeset/strong-badgers-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sap-ux/ui-components': minor
---

Added new SVG icons: SwitchOff and SwitchOn.
Enhanced the UIToggle component by adding visual indicators to display the switch status.
21 changes: 20 additions & 1 deletion packages/ui-components/src/components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ const COLORS = {
warning: 'var(--vscode-notificationsWarningIcon-foreground)',
error: 'var( --vscode-notificationsErrorIcon-foreground)',
info: 'var(--vscode-notificationsInfoIcon-foreground)',
focus: 'var(--vscode-focusBorder)'
focus: 'var(--vscode-focusBorder)',
thumbOn: 'var(--vscode-button-foreground)',
thumbOff: 'var(--vscode-button-secondaryForeground)'
};

export enum UiIcons {
Expand Down Expand Up @@ -140,6 +142,8 @@ export enum UiIcons {
Star = 'Star',
StarActive = 'StarActive',
Success = 'Success',
SwitchOff = 'SwitchOff',
SwitchOn = 'SwitchOn',
Table = 'Table',
Tags = 'Tags',
Task = 'Task',
Expand Down Expand Up @@ -1874,6 +1878,21 @@ export function initIcons(): void {
/>
</svg>
),
[UiIcons.SwitchOff]: (
<svg width="8" height="2" viewBox="0 0 8 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L7 1" stroke={COLORS.thumbOff} strokeLinecap="round" />
</svg>
),
[UiIcons.SwitchOn]: (
<svg width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0.5 3.5L2.5 5.5L7.5 0.5"
stroke={COLORS.thumbOn}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
[UiIcons.Table]: (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path
Expand Down
207 changes: 171 additions & 36 deletions packages/ui-components/src/components/UIToggle/UIToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from 'react';

import ReactDOM from 'react-dom';
import type { IToggleProps, IToggleStyleProps, IToggleStyles } from '@fluentui/react';
import { Toggle } from '@fluentui/react';

import type { UIComponentMessagesProps } from '../../helper/ValidationMessage';
import { getMessageInfo, MessageWrapper } from '../../helper/ValidationMessage';
import { UIIcon } from '../UIIcon';
import { UiIcons } from '../Icons';

export interface UIToggleProps extends IToggleProps, UIComponentMessagesProps {
inlineLabelLeft?: boolean;
Expand All @@ -19,28 +21,46 @@ export enum UIToggleSize {
}

interface UIToggleSizeInfo {
width: number;
height: number;
padding: string;
margin: string;
width?: number;
height?: number;
padding?: string;
margin?: string;
label: {
fontSize: number;
padding: string;
fontSize?: number;
padding?: string;
};
circle: {
width: number;
height: number;
borderWidth: number;
width?: number;
height?: number;
borderWidth?: number;
};
}

const TOGGLE_SIZES = new Map<UIToggleSize, UIToggleSizeInfo>([
[
UIToggleSize.Standard,
{
width: 30,
height: 18,
padding: '0 1px',
margin: '0',
label: {
fontSize: 13,
padding: ''
},
circle: {
width: 14,
height: 14,
borderWidth: 1
}
}
],
[
UIToggleSize.Small,
{
width: 30,
height: 14,
padding: '0 2px',
padding: '0 1px',
margin: '0',
label: {
fontSize: 13,
Expand All @@ -49,37 +69,92 @@ const TOGGLE_SIZES = new Map<UIToggleSize, UIToggleSizeInfo>([
circle: {
width: 10,
height: 10,
borderWidth: 5
borderWidth: 1
}
}
]
]);

const getIconStyleKey = (size: UIToggleSize, isSwitchOn: boolean): string => {
return `${size}-${isSwitchOn ? 'on' : 'off'}`;
};

const ICON_STYLE = new Map<string, React.CSSProperties>([
[
getIconStyleKey(UIToggleSize.Standard, true),
{
position: 'relative',
top: -9,
left: 0
}
],
[
getIconStyleKey(UIToggleSize.Standard, false),
{
position: 'relative',
top: -11,
left: 0
}
],
[
getIconStyleKey(UIToggleSize.Small, true),
{
position: 'relative',
top: -11,
left: 0
}
],
[
getIconStyleKey(UIToggleSize.Small, false),
{
position: 'relative',
top: -13,
left: 0
}
]
]);

const DISABLED_OPACITY = 0.4;

const COLORS = {
pill: {
borderColor: 'var(--vscode-contrastBorder, transparent)',
unchecked: {
background: 'var(--vscode-titleBar-inactiveForeground)',
background: 'var(--vscode-editorWidget-background)',
borderColor: 'var(--vscode-editorWidget-border)',
hover: {
background: 'var(--vscode-editorHint-foreground)',
borderColor: 'var(--vscode-contrastActiveBorder, transparent)'
background: 'var(--vscode-editorWidget-background)',
borderColor: 'var(--vscode-editorWidget-border)'
}
},
checked: {
background: 'var(--vscode-button-background)',
background: 'var(--vscode-editorWidget-background)',
borderColor: 'var(--vscode-contrastActiveBorder, var(--vscode-editorWidget-border))',
hover: {
background: 'var(--vscode-button-hoverBackground)',
borderColor: 'var(--vscode-contrastActiveBorder, transparent)'
background: 'var(--vscode-editorWidget-background)',
borderColor: 'var(--vscode-contrastActiveBorder, var(--vscode-editorWidget-border))'
}
},
focus: {
outline: '1px solid var(--vscode-focusBorder) !important'
}
},
thumb: {
background: 'var(--vscode-button-foreground)'
unchecked: {
background: 'var(--vscode-button-secondaryBackground)',
borderColor: 'var(--vscode-button-border, transparent)',
hover: {
borderColor: 'var(--vscode-button-border, transparent)',
background: 'var(--vscode-contrastBorder, var(--vscode-button-secondaryHoverBackground))'
}
},
checked: {
background: 'var(--vscode-button-background)',
borderColor: 'var(--vscode-contrastActiveBorder, var(--vscode-button-border, transparent))',
hover: {
borderColor: 'var(--vscode-contrastActiveBorder, var(--vscode-button-border, transparent))',
background: 'var(--vscode-contrastActiveBorder, var(--vscode-button-hoverBackground))'
}
}
}
};

Expand All @@ -92,13 +167,60 @@ const COLORS = {
* @extends {React.Component<IToggleProps, {}>}
*/
export class UIToggle extends React.Component<UIToggleProps, {}> {
private readonly toggleRootRef: React.RefObject<HTMLDivElement>;
/**
* Initializes component properties.
*
* @param {UIToggleProps} props
*/
public constructor(props: UIToggleProps) {
super(props);
this.toggleRootRef = React.createRef<HTMLDivElement>();
this.handleChange = this.handleChange.bind(this);
this.replaceThumbWithIcon = this.replaceThumbWithIcon.bind(this);
}

/**
* Lifecycle method called immediately after a component is mounted.
* Executes initialization logic such as DOM manipulations or fetching data.
*
* @returns {void} This method does not return a value.
*/
componentDidMount() {
this.replaceThumbWithIcon();
}

/**
* Handles the change event triggered by the user interaction.
*
* @param {React.MouseEvent<HTMLElement>} event - The mouse event object associated with the interaction.
* @param {boolean} [checked] - An optional parameter indicating the current state of the interaction.
* @returns {void} This method does not return a value.
*/
handleChange(event: React.MouseEvent<HTMLElement>, checked?: boolean) {
this.replaceThumbWithIcon(checked);
this.props.onChange?.(event, checked);
}

/**
* Replaces the thumb element of a toggle switch with an icon based on the toggle's state.
*
* @param {boolean} [checked] Optional. Represents the state of the toggle switch. If not provided, it checks `defaultChecked` prop or defaults to `false`.
* @returns {void} Does not return a value.
*/
replaceThumbWithIcon(checked?: boolean) {
const isSwitchOn = checked ?? this.props.defaultChecked ?? false;
if (this.toggleRootRef.current) {
const thumbElement = (this.toggleRootRef.current as HTMLElement)?.querySelector('.ms-Toggle-thumb');

if (thumbElement) {
const style = ICON_STYLE.get(getIconStyleKey(this.props.size ?? UIToggleSize.Standard, isSwitchOn));
ReactDOM.render(
<UIIcon iconName={isSwitchOn ? UiIcons.SwitchOn : UiIcons.SwitchOff} style={style} />,
thumbElement
);
}
}
}

/**
Expand Down Expand Up @@ -153,33 +275,34 @@ export class UIToggle extends React.Component<UIToggleProps, {}> {
width: sizeInfo?.width,
padding: sizeInfo?.padding,
background: COLORS.pill.checked.background,
borderColor: COLORS.pill.borderColor,
borderColor: COLORS.pill.checked.borderColor,
borderStyle: 'solid',
':hover': {
background: COLORS.pill.checked.hover.background,
borderColor: COLORS.pill.checked.hover.borderColor
},
[`:hover .ms-Toggle-thumb`]: {
backgroundColor: COLORS.thumb.background
},
':disabled': {
background: COLORS.pill.checked.background,
borderColor: COLORS.pill.borderColor,
borderColor: COLORS.pill.checked.borderColor,
opacity: DISABLED_OPACITY
},
...(!styleProps.checked && {
background: COLORS.pill.unchecked.background,
borderStyle: 'dashed',
borderColor: COLORS.pill.unchecked.borderColor,
borderStyle: 'solid',
// This is a bug: the best implementation approach is to set hover styles in the "thumb" section.
// However, the hover styles for the unchecked thumb don't work properly
':hover .ms-Toggle-thumb': {
background: COLORS.thumb.unchecked.hover.background,
borderColor: COLORS.thumb.unchecked.hover.borderColor
},
':hover': {
background: COLORS.pill.unchecked.hover.background,
borderColor: COLORS.pill.unchecked.hover.borderColor
},
[`:hover .ms-Toggle-thumb`]: {
backgroundColor: COLORS.thumb.background
},
':disabled': {
background: COLORS.pill.unchecked.background,
borderColor: COLORS.pill.borderColor,
borderColor: COLORS.pill.unchecked.borderColor,
opacity: DISABLED_OPACITY
}
}),
Expand All @@ -195,19 +318,31 @@ export class UIToggle extends React.Component<UIToggleProps, {}> {
height: sizeInfo?.circle.height,
width: sizeInfo?.circle.width,
borderWidth: sizeInfo?.circle.borderWidth,
background: COLORS.thumb.background,
backgroundPosition: 'center',
borderColor: COLORS.thumb.checked.borderColor,
backgroundColor: COLORS.thumb.checked.background,
':hover': {
backgroundColor: COLORS.thumb.background
}
background: COLORS.thumb.checked.hover.background,
borderColor: COLORS.thumb.checked.hover.borderColor
},
...(!styleProps.checked && {
borderColor: COLORS.thumb.unchecked.borderColor,
backgroundColor: COLORS.thumb.unchecked.background
})
}
};
};

const toogleComponent = <Toggle {...this.props} styles={styles} />;
const toggleComponent = (
<div ref={this.toggleRootRef}>
<Toggle {...this.props} styles={styles} onChange={this.handleChange}></Toggle>
</div>
);

return messageInfo.message ? (
<MessageWrapper message={messageInfo}>{toogleComponent}</MessageWrapper>
<MessageWrapper message={messageInfo}>{toggleComponent}</MessageWrapper>
) : (
toogleComponent
toggleComponent
);
}
}
Loading

0 comments on commit 0f9d186

Please sign in to comment.