Skip to content

Commit f3c5eed

Browse files
authored
[WB-2162] Floating: Add RTL support (#2881)
## Summary: Adds right-to-left support to Floating component. To support this, I've created a custom middleware to be able to mirror the floating placement in RTL contexts. Issue: https://khanacademy.atlassian.net/browse/WB-2162 ## Test plan: Navigate to any floating stories, select the `RTL` mode, and verify that the floating element flips when the `placement` is `left*` or `right*`. Author: jandrade Reviewers: marcysutton, beaesguerra, jandrade Required Reviewers: Approved By: marcysutton, beaesguerra Checks: ✅ 11 checks were successful, ⏭️ 4 checks have been skipped Pull Request URL: #2881
1 parent 4a6e7fb commit f3c5eed

File tree

8 files changed

+541
-15
lines changed

8 files changed

+541
-15
lines changed

.changeset/mighty-penguins-kick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/wonder-blocks-floating": minor
3+
---
4+
5+
Adds right-to-left support to Floating component.

__docs__/wonder-blocks-floating/floating-testing-snapshots.stories.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import {
1111
Portal as PortalStory,
1212
InitialFocus as InitialFocusStory,
1313
CustomStyles as CustomStylesStory,
14+
Placements as PlacementsStory,
1415
} from "./floating.stories";
16+
import {allModes} from "../../.storybook/modes";
1517

1618
const styles = StyleSheet.create({
1719
layout: {
@@ -219,3 +221,19 @@ export const InitialFocus: Story = {
219221
portal: false,
220222
},
221223
};
224+
225+
export const Placements: Story = {
226+
...PlacementsStory,
227+
args: {
228+
...PlacementsStory.args,
229+
open: true,
230+
},
231+
parameters: {
232+
chromatic: {
233+
modes: {
234+
default: allModes.themeDefault,
235+
"default rtl": allModes["themeDefault rtl"],
236+
},
237+
},
238+
},
239+
};

__docs__/wonder-blocks-floating/floating.argtypes.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
import type {ArgTypes} from "@storybook/react-vite";
22

3+
const placements = [
4+
"top",
5+
"top-start",
6+
"top-end",
7+
"right",
8+
"right-start",
9+
"right-end",
10+
"bottom",
11+
"bottom-start",
12+
"bottom-end",
13+
"left",
14+
"left-start",
15+
"left-end",
16+
];
17+
318
export default {
419
content: {
520
control: {type: "text"},
@@ -11,20 +26,8 @@ export default {
1126
},
1227
placement: {
1328
control: {type: "select"},
14-
options: [
15-
"top",
16-
"top-start",
17-
"top-end",
18-
"right",
19-
"right-start",
20-
"right-end",
21-
"bottom",
22-
"bottom-start",
23-
"bottom-end",
24-
"left",
25-
"left-start",
26-
"left-end",
27-
],
29+
mapping: Object.keys(placements),
30+
options: placements,
2831
table: {
2932
type: {
3033
summary: "Placement",

__docs__/wonder-blocks-floating/floating.stories.tsx

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {StyleSheet} from "aphrodite";
33
import * as React from "react";
44

55
import Button from "@khanacademy/wonder-blocks-button";
6-
import {View} from "@khanacademy/wonder-blocks-core";
6+
import {PropsFor, View} from "@khanacademy/wonder-blocks-core";
77
import {Floating} from "@khanacademy/wonder-blocks-floating";
88
import Switch from "@khanacademy/wonder-blocks-switch";
99
import {border, semanticColor, sizing} from "@khanacademy/wonder-blocks-tokens";
@@ -16,6 +16,7 @@ import floatingArgtypes from "./floating.argtypes";
1616
import {ModalLauncher, OnePaneDialog} from "@khanacademy/wonder-blocks-modal";
1717
import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
1818
import {IconMappings} from "../wonder-blocks-icon/phosphor-icon.argtypes";
19+
import IconButton from "@khanacademy/wonder-blocks-icon-button";
1920

2021
type StoryComponentType = StoryObj<typeof Floating>;
2122

@@ -464,6 +465,114 @@ export const CustomStyles: StoryComponentType = {
464465
},
465466
};
466467

468+
type Placement = PropsFor<typeof Floating>["placement"];
469+
470+
/**
471+
* This story demonstrates how to render the floating element in different
472+
* placements.
473+
*
474+
* By default, we set `top` as the placement, but this can be changed by passing
475+
* the `placement` prop.
476+
*
477+
* For more information about the placements, please check the [Floating UI
478+
* documentation](https://floating-ui.com/docs/useFloating#placement).
479+
*
480+
* **NOTE:** In RTL mode, the horizontal placements are mirrored. For example,
481+
* the "left" placement will be rendered as "right" and vice versa.
482+
*/
483+
export const Placements: StoryComponentType = {
484+
args: {
485+
open: true,
486+
},
487+
render: function Render(args, {globals}) {
488+
const isRTL = globals.direction === "rtl";
489+
const placements: Array<{name: Placement; icon: PhosphorRegular}> = [
490+
{
491+
name: "top-start",
492+
icon: isRTL
493+
? IconMappings.arrowElbowRightUp
494+
: IconMappings.arrowElbowLeftUp,
495+
},
496+
{name: "top", icon: IconMappings.arrowUp},
497+
{
498+
name: "top-end",
499+
icon: isRTL
500+
? IconMappings.arrowElbowLeftUp
501+
: IconMappings.arrowElbowRightUp,
502+
},
503+
{
504+
name: "right-start",
505+
icon: isRTL
506+
? IconMappings.arrowElbowDownLeft
507+
: IconMappings.arrowElbowUpRight,
508+
},
509+
{
510+
name: "right",
511+
icon: isRTL ? IconMappings.arrowLeft : IconMappings.arrowRight,
512+
},
513+
{
514+
name: "right-end",
515+
icon: isRTL
516+
? IconMappings.arrowElbowUpLeft
517+
: IconMappings.arrowElbowDownRight,
518+
},
519+
{
520+
name: "bottom-start",
521+
icon: isRTL
522+
? IconMappings.arrowElbowRightDown
523+
: IconMappings.arrowElbowLeftDown,
524+
},
525+
{name: "bottom", icon: IconMappings.arrowDown},
526+
{
527+
name: "bottom-end",
528+
icon: isRTL
529+
? IconMappings.arrowElbowLeftDown
530+
: IconMappings.arrowElbowRightDown,
531+
},
532+
{
533+
name: "left-start",
534+
icon: isRTL
535+
? IconMappings.arrowElbowDownRight
536+
: IconMappings.arrowElbowUpLeft,
537+
},
538+
{
539+
name: "left",
540+
icon: isRTL ? IconMappings.arrowRight : IconMappings.arrowLeft,
541+
},
542+
{
543+
name: "left-end",
544+
icon: isRTL
545+
? IconMappings.arrowElbowUpRight
546+
: IconMappings.arrowElbowDownLeft,
547+
},
548+
];
549+
550+
return (
551+
<View style={styles.placementsContainer}>
552+
{placements.map(({name, icon}) => (
553+
<Floating
554+
{...args}
555+
content={
556+
<View style={[styles.contentContainer, styles.row]}>
557+
<BodyText>{name}</BodyText>
558+
<PhosphorIcon icon={IconMappings.cookie} />
559+
</View>
560+
}
561+
placement={name}
562+
key={name}
563+
>
564+
<IconButton
565+
actionType="neutral"
566+
aria-label={name}
567+
icon={<PhosphorIcon icon={icon} />}
568+
/>
569+
</Floating>
570+
))}
571+
</View>
572+
);
573+
},
574+
};
575+
467576
const styles = StyleSheet.create({
468577
storyCanvas: {
469578
minHeight: 200,
@@ -511,4 +620,12 @@ const styles = StyleSheet.create({
511620
padding: sizing.size_160,
512621
gap: sizing.size_160,
513622
},
623+
placementsContainer: {
624+
display: "grid",
625+
gridTemplateColumns: "repeat(3, 1fr)",
626+
gridTemplateRows: "repeat(4, 150px)",
627+
placeItems: "center",
628+
inlineSize: "100%",
629+
blockSize: "100%",
630+
},
514631
});

__docs__/wonder-blocks-icon/phosphor-icon.argtypes.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ import gear from "@phosphor-icons/core/regular/gear.svg";
5252
import cookie from "@phosphor-icons/core/regular/cookie.svg";
5353
import cookieBold from "@phosphor-icons/core/bold/cookie-bold.svg";
5454
import iceCream from "@phosphor-icons/core/regular/ice-cream.svg";
55+
import arrowRight from "@phosphor-icons/core/regular/arrow-right.svg";
56+
import arrowLeft from "@phosphor-icons/core/regular/arrow-left.svg";
57+
import arrowElbowDownLeft from "@phosphor-icons/core/regular/arrow-elbow-down-left.svg";
58+
import arrowElbowDownRight from "@phosphor-icons/core/regular/arrow-elbow-down-right.svg";
59+
import arrowElbowUpLeft from "@phosphor-icons/core/regular/arrow-elbow-up-left.svg";
60+
import arrowElbowUpRight from "@phosphor-icons/core/regular/arrow-elbow-up-right.svg";
61+
import arrowElbowLeftDown from "@phosphor-icons/core/regular/arrow-elbow-left-down.svg";
62+
import arrowElbowLeftUp from "@phosphor-icons/core/regular/arrow-elbow-left-up.svg";
63+
import arrowElbowRightDown from "@phosphor-icons/core/regular/arrow-elbow-right-down.svg";
64+
import arrowElbowRightUp from "@phosphor-icons/core/regular/arrow-elbow-right-up.svg";
5565

5666
import {semanticColor} from "@khanacademy/wonder-blocks-tokens";
5767
import {flattenNestedTokens} from "../components/tokens-util";
@@ -112,6 +122,16 @@ export const IconMappings = {
112122
cookie,
113123
cookieBold,
114124
iceCream,
125+
arrowRight,
126+
arrowLeft,
127+
arrowElbowDownLeft,
128+
arrowElbowDownRight,
129+
arrowElbowUpLeft,
130+
arrowElbowUpRight,
131+
arrowElbowLeftDown,
132+
arrowElbowLeftUp,
133+
arrowElbowRightDown,
134+
arrowElbowRightUp,
115135
} as const;
116136

117137
// We flatten the tokens and filter out the colors that are not relevant to

packages/wonder-blocks-floating/src/components/floating.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {addStyle, StyleType} from "@khanacademy/wonder-blocks-core";
2222
import {ARROW_SIZE_INLINE} from "../util/constants";
2323
import {Arrow, type ArrowStyles} from "./floating-arrow";
2424
import {Portal} from "./floating-portal";
25+
import {rtlMirror} from "../util/rtl-mirror-middleware";
2526

2627
const StyledDiv = addStyle("div");
2728

@@ -243,6 +244,8 @@ export default function Floating({
243244
: undefined,
244245
showArrow ? arrow({element: arrowRef}) : undefined,
245246
hideProp ? hide() : undefined,
247+
// Mirror the floating element in RTL when placement is left/right
248+
rtlMirror(),
246249
],
247250
});
248251

0 commit comments

Comments
 (0)