Skip to content

Commit bb3d343

Browse files
committed
feat: add RTL support to Switch component and enhance tooltip visibility
- Implemented RTL (Right-to-Left) layout support in the Switch component, allowing for better internationalization. - Updated SCSS styles to ensure tooltips overflow correctly and remain visible in various components. - Enhanced documentation to reflect new RTL feature and its usage in the Switch component examples.
1 parent c74adb4 commit bb3d343

File tree

14 files changed

+270
-10
lines changed

14 files changed

+270
-10
lines changed

packages/documentation/App.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,9 @@ pre {
412412
width: 100% !important;
413413
box-shadow: var(--shadow-card) !important;
414414
transition: all var(--transition-base) !important;
415-
overflow: hidden !important;
415+
/* Allow tooltips to overflow and be visible */
416+
overflow-x: hidden !important;
417+
overflow-y: visible !important;
416418

417419
&:hover {
418420
box-shadow: var(--shadow-card-hover) !important;
@@ -443,6 +445,8 @@ pre {
443445
align-items: flex-start;
444446
width: 100%;
445447
overflow-x: auto;
448+
/* Allow tooltips to overflow vertically */
449+
overflow-y: visible;
446450
}
447451

448452
.rc-demo-widgets-wrapper {

packages/documentation/common/demo-page-renderer/components/demo-container.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
border: 1px solid var(--border-color);
1010
border-radius: 12px;
1111
box-shadow: var(--shadow-md);
12-
overflow: hidden;
12+
/* Use overflow-x: hidden to maintain rounded corners, but allow overflow-y for tooltips */
13+
overflow-x: hidden;
14+
overflow-y: visible;
1315
transition: all var(--transition-base);
1416

1517
&:hover {
@@ -133,6 +135,8 @@
133135
align-items: center;
134136
justify-content: center;
135137
transition: background var(--transition-base);
138+
/* Allow tooltips to overflow and be visible */
139+
overflow: visible;
136140
}
137141

138142
.demo-container__demo-inner {

packages/documentation/common/demo-page-renderer/components/demo-container.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { FunctionComponent, ReactNode, useState } from 'react';
22
import classNames from 'classnames';
33
import { Smartphone, Tablet, Monitor } from 'lucide-react';
44
import type { ViewportSize, DemoControls } from '../types';
5+
import { addRTLToElement } from '../utils/rtl-wrapper';
56
import './demo-container.scss';
67

78
export interface DemoContainerProps {
@@ -154,7 +155,7 @@ const DemoContainer: FunctionComponent<DemoContainerProps> = ({
154155
'demo-container__demo-inner--tablet': viewport === 'tablet',
155156
})}
156157
>
157-
{children}
158+
{addRTLToElement(children)}
158159
</div>
159160
</div>
160161
</div>

packages/documentation/common/demo-page-renderer/utils/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ export {
3030
variantToCodeSnippet,
3131
} from './code-extractor';
3232
export type { JsxToStringOptions } from './code-extractor';
33+
34+
// RTL wrapper
35+
export { addRTLToElement, RTLWrapper } from './rtl-wrapper';
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import React, { ReactElement, ReactNode, isValidElement, cloneElement } from 'react';
2+
3+
/**
4+
* List of component names that support RTL prop
5+
* This is based on the components found in the codebase that have RTL?: boolean in their props
6+
*/
7+
const RTL_SUPPORTED_COMPONENTS = new Set([
8+
'Switch',
9+
'Input',
10+
'Password',
11+
'InputNumber',
12+
'FormField',
13+
'FormGroup',
14+
'Checkbox',
15+
'CheckboxGroup',
16+
'Radio',
17+
'RadioGroup',
18+
'Dropdown',
19+
'Menu',
20+
'MenuButton',
21+
'MenuBar',
22+
'BreadCrumb',
23+
'Section',
24+
'PageHeader',
25+
'Text',
26+
'Alert',
27+
'Skeleton',
28+
'Rate',
29+
'Progress',
30+
'Transfer',
31+
'FileUpload',
32+
'Pin',
33+
'ReadMore',
34+
'List',
35+
'Tags',
36+
'Slider',
37+
'AutoSuggest',
38+
'LoadingIndicator',
39+
'Accordion',
40+
'AccordionGroup',
41+
]);
42+
43+
/**
44+
* Gets the component name from a React element type
45+
*/
46+
function getComponentName(componentType: any): string | null {
47+
if (!componentType) {
48+
return null;
49+
}
50+
51+
// Direct function component
52+
if (typeof componentType === 'function') {
53+
return componentType.displayName || componentType.name || null;
54+
}
55+
56+
// For forwardRef and memo components, check the inner component
57+
if (typeof componentType === 'object') {
58+
// React.memo
59+
if (componentType.type) {
60+
const innerName = getComponentName(componentType.type);
61+
if (innerName) return innerName;
62+
}
63+
// React.forwardRef
64+
if (componentType.render) {
65+
const innerName = getComponentName(componentType.render);
66+
if (innerName) return innerName;
67+
}
68+
// Check displayName directly on the object
69+
if (componentType.displayName) {
70+
return componentType.displayName;
71+
}
72+
}
73+
74+
return null;
75+
}
76+
77+
/**
78+
* Checks if a component supports RTL by checking its displayName or type name
79+
*/
80+
function componentSupportsRTL(element: ReactElement): boolean {
81+
if (!isValidElement(element)) {
82+
return false;
83+
}
84+
85+
const componentType = element.type;
86+
87+
// Check if it's a string component (e.g., 'div', 'span') - these don't support RTL
88+
if (typeof componentType === 'string') {
89+
return false;
90+
}
91+
92+
// Get component name
93+
const componentName = getComponentName(componentType);
94+
95+
if (componentName && RTL_SUPPORTED_COMPONENTS.has(componentName)) {
96+
return true;
97+
}
98+
99+
return false;
100+
}
101+
102+
/**
103+
* Recursively adds RTL prop to all React elements that support it
104+
*
105+
* @param node - React node to process
106+
* @returns React node with RTL prop added where applicable
107+
*/
108+
export function addRTLToElement(node: ReactNode): ReactNode {
109+
// Handle null, undefined, or non-elements
110+
if (!node || !isValidElement(node)) {
111+
return node;
112+
}
113+
114+
const element = node as ReactElement;
115+
116+
// Check if this element supports RTL and doesn't already have RTL set
117+
const supportsRTL = componentSupportsRTL(element);
118+
const hasRTL = element.props && 'RTL' in element.props;
119+
const rtlValue = element.props?.RTL;
120+
121+
// Process children first (before cloning)
122+
let processedChildren = element.props?.children;
123+
if (processedChildren) {
124+
// Handle array of children
125+
if (Array.isArray(processedChildren)) {
126+
processedChildren = processedChildren.map(child => addRTLToElement(child));
127+
}
128+
// Handle single child
129+
else if (processedChildren !== null && processedChildren !== undefined) {
130+
processedChildren = addRTLToElement(processedChildren);
131+
}
132+
}
133+
134+
// Clone element with RTL prop if it supports it and doesn't have it (or has it as false)
135+
let clonedElement = element;
136+
if (supportsRTL) {
137+
if (!hasRTL || rtlValue === false) {
138+
// Clone with RTL=true, preserving all other props and processed children
139+
clonedElement = cloneElement(
140+
element,
141+
{
142+
...element.props,
143+
RTL: true,
144+
children: processedChildren,
145+
} as any
146+
);
147+
} else if (processedChildren !== element.props?.children) {
148+
// Children were processed, need to update them even if RTL is already true
149+
clonedElement = cloneElement(
150+
element,
151+
{
152+
...element.props,
153+
children: processedChildren,
154+
} as any
155+
);
156+
}
157+
} else if (processedChildren !== element.props?.children) {
158+
// Component doesn't support RTL but children were processed
159+
clonedElement = cloneElement(
160+
element,
161+
{
162+
...element.props,
163+
children: processedChildren,
164+
} as any
165+
);
166+
}
167+
168+
return clonedElement;
169+
}
170+
171+
/**
172+
* Wrapper component that adds RTL to all children
173+
*/
174+
export const RTLWrapper: React.FunctionComponent<{ children: ReactNode }> = ({
175+
children,
176+
}) => {
177+
return <>{addRTLToElement(children)}</>;
178+
};

packages/documentation/common/inline-code-viewer/header-code-toggle.scss

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
align-items: center;
1515
flex-shrink: 0;
1616
justify-content: flex-end;
17-
width: auto;
17+
width: 180px;
1818
margin-bottom: 0;
1919
margin-left: auto;
2020
}
@@ -52,6 +52,8 @@
5252
opacity: 1;
5353
transform: translateY(0);
5454
transition: opacity 0.3s ease, transform 0.3s ease;
55+
direction: ltr; /* Code should always be left-to-right */
56+
text-align: left; /* Ensure code text aligns left */
5557
}
5658

5759
.header-code-toggle__content.is-component-view .header-code-toggle__code-view {

packages/documentation/common/inline-code-viewer/header-code-toggle.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ const HeaderCodeToggleButton: FunctionComponent<
103103
label={switchLabel}
104104
size={size}
105105
aria-label={isCodeView ? 'Show preview' : 'Show code'}
106-
style={{ transform: 'scale(0.9)', transformOrigin: 'right center' }}
106+
width={60}
107107
/>
108108
</div>
109109
);

packages/documentation/common/syntax-highlighter/syntax-highlighter.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@
111111
overflow-x: auto;
112112
scrollbar-width: thin;
113113
scrollbar-color: var(--border-color) transparent;
114+
direction: ltr; /* Code should always be left-to-right */
115+
text-align: left; /* Ensure code text aligns left */
114116

115117
&::-webkit-scrollbar {
116118
height: 8px;
@@ -140,6 +142,8 @@
140142
line-height: 1.6 !important;
141143
overflow-x: auto;
142144
counter-reset: line;
145+
direction: ltr !important; /* Code should always be left-to-right */
146+
text-align: left !important; /* Ensure code text aligns left */
143147

144148
code {
145149
font-family: var(--font-mono) !important;

packages/documentation/components/switch/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ function switchComponent() {
1212
The design and functionality of this control is based on a physical switch that allows users to turn things ON or OFF`}
1313
sourceId="switch/switch.tsx"
1414
editId="switch"
15-
features={['Custom sizes', 'ARIA labels', 'Disabled state']}
15+
features={['Custom sizes', 'ARIA labels', 'Disabled state', 'RTL support']}
1616
callbacks={[
1717
{
1818
default: ``,
@@ -65,6 +65,13 @@ function switchComponent() {
6565
optional: 'Yes',
6666
type: 'Object',
6767
},
68+
{
69+
default: 'False',
70+
description: `enables right-to-left layout mode for internationalization`,
71+
name: 'RTL',
72+
optional: 'Yes',
73+
type: 'Boolean',
74+
},
6875
]}
6976
tabTitles={['Examples', 'Properties', 'Playground']}
7077
stackBlitzCodes={['react-ts-p8rf9h']}

packages/documentation/components/switch/widget-variants.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,13 @@ export const Large = (
6666
onChange={val => console.log('Marketing:', val)}
6767
/>
6868
);
69+
70+
export const RTL = (
71+
<Switch
72+
label="وضع الظلام"
73+
checked
74+
onChange={val => console.log('Dark mode:', val)}
75+
size="sm"
76+
RTL
77+
/>
78+
);

0 commit comments

Comments
 (0)