Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add visual status indicators to the UIToggle component #2698

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
dfdd35f
feat(ui-components): replace toggle thumb with custom icon
Vladislavs-Silins-SAP Dec 11, 2024
b32d020
feat(ui-toggle): add icon support for toggle states
Vladislavs-Silins-SAP Dec 11, 2024
11ca5fb
feat: add customizable colors for switch icons
Vladislavs-Silins-SAP Dec 11, 2024
e6af4ed
refactor: remove UIToggle.scss and inline styles
Vladislavs-Silins-SAP Dec 12, 2024
fa2dfd7
fix(ui-toggle): standardize default styles for UIToggle
Vladislavs-Silins-SAP Dec 12, 2024
5b19a8c
refactor(ui-toggle): make size info properties optional
Vladislavs-Silins-SAP Dec 12, 2024
851b352
feat: add new SVG icons and enhance UIToggle component
Vladislavs-Silins-SAP Dec 12, 2024
e7149d3
Linting auto fix commit
github-actions[bot] Dec 12, 2024
f0e57f9
refactor: remove unused icon definitions in Icons.tsx
Vladislavs-Silins-SAP Dec 12, 2024
c158488
Merge branch 'feat/32111-bug-acc-2531level-aa_sap-fiori-tools_switch-…
Vladislavs-Silins-SAP Dec 12, 2024
764ca0a
feat(ui-toggle): update styles for hover and focus states
Vladislavs-Silins-SAP Dec 13, 2024
b0e966b
fix(ui-toggle): improve hover styles and theme handling
Vladislavs-Silins-SAP Dec 13, 2024
f8fc016
fix: update UIToggle styles for consistency and clarity
Vladislavs-Silins-SAP Dec 13, 2024
929ded1
fix(ui-toggle): adjust padding and border color handling
Vladislavs-Silins-SAP Dec 16, 2024
9397f8a
fix(ui-toggle): adjust padding and border color styles
Vladislavs-Silins-SAP Dec 16, 2024
a441d3d
fix: update UIToggle to use UiIcons constants
Vladislavs-Silins-SAP Dec 16, 2024
5c78cf5
refactor: add readonly to toggleRootRef in UIToggle
Vladislavs-Silins-SAP Dec 16, 2024
27dd3b2
Merge branch 'main' into feat/32111-bug-acc-2531level-aa_sap-fiori-to…
Vladislavs-Silins-SAP Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading