From d7308aebec1e3ff22df548b5613f058c9ba35262 Mon Sep 17 00:00:00 2001
From: Michael Coker <35148959+mcoker@users.noreply.github.com>
Date: Thu, 2 Nov 2023 09:49:39 -0500
Subject: [PATCH] fix(RTL): added right-to-left page demo (#9694)
---
packages/react-core/src/demos/RTL/RTL.md | 31 ++
.../src/demos/RTL/examples/PaginatedTable.css | 7 +
.../src/demos/RTL/examples/PaginatedTable.jsx | 507 ++++++++++++++++++
.../demos/RTL/examples/translations.en.json | 67 +++
.../demos/RTL/examples/translations.he.json | 82 +++
5 files changed, 694 insertions(+)
create mode 100644 packages/react-core/src/demos/RTL/RTL.md
create mode 100644 packages/react-core/src/demos/RTL/examples/PaginatedTable.css
create mode 100644 packages/react-core/src/demos/RTL/examples/PaginatedTable.jsx
create mode 100644 packages/react-core/src/demos/RTL/examples/translations.en.json
create mode 100644 packages/react-core/src/demos/RTL/examples/translations.he.json
diff --git a/packages/react-core/src/demos/RTL/RTL.md b/packages/react-core/src/demos/RTL/RTL.md
new file mode 100644
index 00000000000..7285d2b3f36
--- /dev/null
+++ b/packages/react-core/src/demos/RTL/RTL.md
@@ -0,0 +1,31 @@
+---
+id: Right-to-left
+section: patterns
+---
+
+import translationsEn from "./examples/translations.en.json";
+import translationsHe from "./examples/translations.he.json";
+import AlignRightIcon from '@patternfly/react-icons/dist/esm/icons/align-right-icon';
+import ToolsIcon from '@patternfly/react-icons/dist/esm/icons/tools-icon';
+import ClockIcon from '@patternfly/react-icons/dist/esm/icons/clock-icon';
+import WalkingIcon from '@patternfly/react-icons/dist/esm/icons/walking-icon';
+import pfLogo from '@patternfly/react-core/src/demos/assets/pf-logo.svg';
+import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon';
+import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon';
+import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon';
+import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon';
+import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
+import HandPaperIcon from '@patternfly/react-icons/dist/esm/icons/hand-paper-icon';
+import imgAvatar from '@patternfly/react-core/src/components/assets/avatarImg.svg';
+
+import './examples/PaginatedTable.css';
+
+## Demos
+
+This demonstrates how the UI adapts to the writing mode of the page.
+
+### Paginated table
+
+```js file="./examples/PaginatedTable.jsx" isFullscreen
+
+```
diff --git a/packages/react-core/src/demos/RTL/examples/PaginatedTable.css b/packages/react-core/src/demos/RTL/examples/PaginatedTable.css
new file mode 100644
index 00000000000..adbc05697eb
--- /dev/null
+++ b/packages/react-core/src/demos/RTL/examples/PaginatedTable.css
@@ -0,0 +1,7 @@
+.brand-language {
+ font-weight: var(--pf-v5-global--FontWeight--bold);
+ font-family: var(--pf-v5-global--FontFamily--heading);
+ font-size: var(--pf-v5-global--FontSize--xl);
+ align-self: center;
+ margin-inline-start: var(--pf-v5-global--spacer--md);
+}
\ No newline at end of file
diff --git a/packages/react-core/src/demos/RTL/examples/PaginatedTable.jsx b/packages/react-core/src/demos/RTL/examples/PaginatedTable.jsx
new file mode 100644
index 00000000000..3199822b294
--- /dev/null
+++ b/packages/react-core/src/demos/RTL/examples/PaginatedTable.jsx
@@ -0,0 +1,507 @@
+import * as React from 'react';
+
+import {
+ Avatar,
+ Brand,
+ Breadcrumb,
+ BreadcrumbItem,
+ Button,
+ ButtonVariant,
+ Card,
+ Divider,
+ Dropdown,
+ DropdownGroup,
+ DropdownItem,
+ DropdownList,
+ Icon,
+ Label,
+ Masthead,
+ MastheadBrand,
+ MastheadContent,
+ MastheadMain,
+ MastheadToggle,
+ MenuToggle,
+ Nav,
+ NavItem,
+ NavList,
+ Page,
+ PageBreadcrumb,
+ PageSection,
+ PageSidebar,
+ PageSidebarBody,
+ PageToggleButton,
+ Pagination,
+ PaginationVariant,
+ Text,
+ TextContent,
+ TextVariants,
+ Toolbar,
+ ToolbarContent,
+ ToolbarGroup,
+ ToolbarItem,
+ Truncate
+} from '@patternfly/react-core';
+
+import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
+import translationsEn from './examples/translations.en.json';
+import translationsHe from './examples/translations.he.json';
+import AlignRightIcon from '@patternfly/react-icons/dist/esm/icons/align-right-icon';
+import ToolsIcon from '@patternfly/react-icons/dist/esm/icons/tools-icon';
+import ClockIcon from '@patternfly/react-icons/dist/esm/icons/clock-icon';
+import WalkingIcon from '@patternfly/react-icons/dist/esm/icons/walking-icon';
+import pfLogo from '@patternfly/react-core/src/demos/assets/pf-logo.svg';
+import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon';
+import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon';
+import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon';
+import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon';
+import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
+import HandPaperIcon from '@patternfly/react-icons/dist/esm/icons/hand-paper-icon';
+import imgAvatar from '@patternfly/react-core/src/components/assets/avatarImg.svg';
+
+export const PaginatedTableAction = () => {
+ const [translation, setTranslation] = React.useState(translationsEn);
+ const [page, setPage] = React.useState(1);
+ const [perPage, setPerPage] = React.useState(10);
+
+ const columns = [
+ translation.table.columns.servers,
+ translation.table.columns.status,
+ translation.table.columns.location,
+ translation.table.columns.modified,
+ translation.table.columns.url
+ ];
+
+ const numRows = 25;
+ const getRandomInteger = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
+ const createRows = () => {
+ const rows = [];
+ for (let i = 0; i < numRows; i++) {
+ const num = i + 1;
+ const rowObj = {
+ name: translation.table.rows.node + num,
+ status: [
+ translation.table.rows.status.stopped,
+ translation.table.rows.status.running,
+ translation.table.rows.status.down,
+ translation.table.rows.status.needsMaintenance
+ ][getRandomInteger(0, 3)],
+ location: [
+ translation.table.rows.locations.raleigh,
+ translation.table.rows.locations.boston,
+ translation.table.rows.locations.atlanta,
+ translation.table.rows.locations.sanFrancisco
+ ][getRandomInteger(0, 3)],
+ lastModified: [
+ translation.table.rows.lastModified.oneHr,
+ translation.table.rows.lastModified.threeHrs,
+ translation.table.rows.lastModified.fiveHrs,
+ translation.table.rows.lastModified.sevenMins,
+ translation.table.rows.lastModified.fortyTwoMins,
+ translation.table.rows.lastModified.twoDays,
+ translation.table.rows.lastModified.oneMonth
+ ][getRandomInteger(0, 6)],
+ url: 'http://www.redhat.com/en/office-locations/node' + num
+ };
+ rows.push(rowObj);
+ }
+
+ return rows;
+ };
+
+ const rows = createRows();
+ const [managedRows, setManagedRows] = React.useState(rows);
+ const [paginatedRows, setPaginatedRows] = React.useState(rows.slice(0, 10));
+ const [isDirRTL, setIsDirRTL] = React.useState(false);
+
+ const capitalize = (input) => input[0].toUpperCase() + input.substring(1);
+
+ const switchTranslation = () => {
+ setIsDirRTL((prevIsDirRTL) => !prevIsDirRTL);
+ setTranslation((prevTranslation) => (prevTranslation === translationsEn ? translationsHe : translationsEn));
+ };
+
+ React.useEffect(() => {
+ const newRows = createRows();
+ setManagedRows(newRows);
+ setPaginatedRows(newRows.slice((page - 1) * perPage, page * perPage));
+ }, [translation]);
+
+ React.useEffect(() => {
+ const html = document.querySelector('html');
+ html.dir = isDirRTL ? 'rtl' : 'ltr';
+ }, [isDirRTL]);
+
+ // Pagination logic
+
+ const handleSetPage = (_evt, newPage, _perPage, startIdx, endIdx) => {
+ setPaginatedRows(managedRows.slice(startIdx, endIdx));
+ setPage(newPage);
+ };
+
+ const handlePerPageSelect = (_evt, newPerPage, _newPage, startIdx, endIdx) => {
+ setPaginatedRows(managedRows.slice(startIdx, endIdx));
+ setPerPage(newPerPage);
+ };
+
+ const renderPagination = (variant) => {
+ const { pagination } = translation;
+
+ return (
+
+ );
+ };
+
+ const breadcrumbItems = {
+ home: {
+ url: '#home'
+ },
+ category: {
+ url: '#category'
+ },
+ subCategory: {
+ url: 'sub-category'
+ }
+ };
+
+ const renderLabel = (labelText) => {
+ switch (labelText) {
+ case 'Running':
+ case 'רץ':
+ return (
+
+
+
+ }
+ >
+ {translation.table.rows.status.running}
+
+ );
+ case 'Stopped':
+ case 'עצר':
+ return (
+
+
+
+ }
+ color="red"
+ >
+ {translation.table.rows.status.stopped}
+
+ );
+ case 'Needs maintenance':
+ case 'זקוק לתחזוקה':
+ return (
+ } color="blue">
+ {translation.table.rows.status.needsMaintenance}
+
+ );
+ case 'Down':
+ case 'מטה':
+ return (
+ } color="orange">
+ {translation.table.rows.status.down}
+
+ );
+ }
+ };
+
+ const toolbarItems = (
+
+
+
+
+
+
+
+ }
+ iconPosition="end"
+ onClick={switchTranslation}
+ >
+ {translation.switchBtn}
+
+
+ {renderPagination(PaginationVariant.top)}
+
+
+
+ );
+
+ const pageNav = (
+
+
+
+ {translation.nav.systemPanel}
+
+
+ {translation.nav.policy}
+
+
+ {translation.nav.authentication}
+
+
+ {translation.nav.networkServices}
+
+
+ {translation.nav.server}
+
+
+
+ );
+
+ const sidebar = (
+
+ {pageNav}
+
+ );
+
+ const [isDropdownOpen, setIsDropdownOpen] = React.useState(false);
+ const [isKebabDropdownOpen, setIsKebabDropdownOpen] = React.useState(false);
+ const [isFullKebabDropdownOpen, setIsFullKebabDropdownOpen] = React.useState(false);
+
+ const kebabDropdownItems = (
+ <>
+ }>{translation.kebabDropdown.settings}
+ }>{translation.kebabDropdown.help}
+ >
+ );
+
+ const userDropdownItems = (
+ <>
+ {translation.userDropdown.myProfile}
+ {translation.userDropdown.userManagement}
+ {translation.userDropdown.logout}
+ >
+ );
+
+ const onDropdownToggle = () => {
+ setIsDropdownOpen(!isDropdownOpen);
+ };
+
+ const onDropdownSelect = () => {
+ setIsDropdownOpen(false);
+ };
+
+ const onKebabDropdownToggle = () => {
+ setIsKebabDropdownOpen(!isKebabDropdownOpen);
+ };
+
+ const onKebabDropdownSelect = () => {
+ setIsKebabDropdownOpen(false);
+ };
+
+ const onFullKebabToggle = () => {
+ setIsFullKebabDropdownOpen(!isFullKebabDropdownOpen);
+ };
+
+ const onFullKebabSelect = () => {
+ setIsFullKebabDropdownOpen(false);
+ };
+
+ const masthead = (
+
+
+
+
+
+
+
+
+
+ {translation.brandLanguage && {translation.brandLanguage} }
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+
+ (
+
+
+
+ )}
+ >
+ {kebabDropdownItems}
+
+
+
+ (
+
+
+
+ )}
+ >
+
+ {userDropdownItems}
+
+
+ {kebabDropdownItems}
+
+
+
+
+ (
+ }
+ isFullHeight
+ >
+ {translation.username}
+
+ )}
+ >
+ {userDropdownItems}
+
+
+
+
+
+
+ );
+
+ return (
+
+
+
+
+ {Object.keys(breadcrumbItems).map((key, idx, arr) => (
+
+ {translation.breadcrumbs[key]}
+ {breadcrumbItems.length}
+
+ ))}
+
+
+
+
+ {translation.title}
+ {translation.body}
+
+
+
+
+ {toolbarItems}
+
+
+
+ {columns.map((column, columnIndex) => (
+ {column}
+ ))}
+
+
+
+ {paginatedRows.map((row, rowIndex) => (
+
+ <>
+ {Object.entries(row).map(([key, value]) => {
+ if (key === 'status') {
+ return (
+
+ {renderLabel(value)}
+
+ );
+ } else if (key === 'url') {
+ return (
+ // Passing dir="rtl" forces truncation at the start of the URL,
+ // resulting in the unique portion being visible regardless of language
+
+
+
+
+
+ );
+ } else {
+ return (
+
+ {value}
+
+ );
+ }
+ })}
+ >
+
+ ))}
+
+
+ {renderPagination(PaginationVariant.bottom)}
+
+
+
+
+ );
+};
diff --git a/packages/react-core/src/demos/RTL/examples/translations.en.json b/packages/react-core/src/demos/RTL/examples/translations.en.json
new file mode 100644
index 00000000000..b60411c7a74
--- /dev/null
+++ b/packages/react-core/src/demos/RTL/examples/translations.en.json
@@ -0,0 +1,67 @@
+{
+ "title": "Main title",
+ "username": "John Doe",
+ "body": "This is a full page demo.",
+ "mastheadToggleAriaLabel": "Global navigation",
+ "kebabDropdown": {
+ "settings": "Settings",
+ "help": "Help",
+ "settingsAndHelp": "Settings and help"
+ },
+ "userDropdown": {
+ "myProfile": "My profile",
+ "userManagement": "User management",
+ "logout": "Log out"
+ },
+ "kebabAndUserDropdown": {
+ "toolbarMenuAriaLabel": "Toolbar menu",
+ "groupAriaLabel": "User actions"
+ },
+ "breadcrumbs": {
+ "home": "Home",
+ "category": "Category",
+ "subCategory": "Sub category"
+ },
+ "nav": {
+ "systemPanel": "System panel",
+ "policy": "Policy",
+ "authentication": "Authentication",
+ "networkServices": "Network services",
+ "server": "Server"
+ },
+ "switchBtn": "Switch to Hebrew",
+ "table": {
+ "ariaLabel": "Right to left demo table",
+ "columns": {
+ "servers": "Servers",
+ "status": "Status",
+ "location": "Location",
+ "modified": "Last modified",
+ "url": "URL"
+ },
+ "rows": {
+ "node": "Node",
+ "status": {
+ "stopped": "Stopped",
+ "running": "Running",
+ "down": "Down",
+ "needsMaintenance": "Needs maintenance"
+ },
+ "locations": {
+ "raleigh": "Raleigh",
+ "boston": "Boston",
+ "atlanta": "Atlanta",
+ "sanFrancisco": "San Francisco"
+ },
+ "lastModified": {
+ "oneHr": "1 hour ago",
+ "threeHrs": "3 hours ago",
+ "fiveHrs": "5 hours ago",
+ "sevenMins": "7 minutes ago",
+ "fortyTwoMins": "42 minutes ago",
+ "twoDays": "2 days ago",
+ "oneMonth": "1 month ago"
+ }
+ }
+ }
+}
diff --git a/packages/react-core/src/demos/RTL/examples/translations.he.json b/packages/react-core/src/demos/RTL/examples/translations.he.json
new file mode 100644
index 00000000000..b7783400856
--- /dev/null
+++ b/packages/react-core/src/demos/RTL/examples/translations.he.json
@@ -0,0 +1,82 @@
+{
+ "title": "כותרת ראשית",
+ "username": "פלוני אלמוני",
+ "brandLanguage": "עִברִית",
+ "body": "זוהי הדגמה של עמוד שלם.",
+ "mastheadToggleAriaLabel": "ניווט גלובלי",
+ "kebabDropdown": {
+ "settings": "הגדרות",
+ "help": "עֶזרָה",
+ "settingsAndHelp": "הגדרות ועזרה"
+ },
+ "userDropdown": {
+ "myProfile": "הפרופיל שלי",
+ "userManagement": "ניהול משתמשים",
+ "logout": "להתנתק"
+ },
+ "kebabAndUserDropdown": {
+ "toolbarMenuAriaLabel": "תפריט סרגל הכלים",
+ "groupAriaLabel": "פעולות משתמש"
+ },
+ "breadcrumbs": {
+ "home": "בית",
+ "category": "קטגוריה",
+ "subCategory": "קטגוריית משנה",
+ "ariaLabel": "פירורי לחם"
+ },
+ "pagination": {
+ "ofWord": "שֶׁל",
+ "items": "פריטים",
+ "perPageSuffix": "לכל עמוד",
+ "toNextPageAriaLabel": "עבור לדף הבא",
+ "toPreviousPageAriaLabel": "תחזור לדף הקודם",
+ "toFirstPageAriaLabel": "עבור לעמוד הראשון",
+ "toLastPageAriaLabel": "עבור לדף האחרון",
+ "currPageAriaLabel": "העמוד הנוכחי",
+ "topVariantAriaLabel": "עימוד עליון",
+ "bottomVariantAriaLabel": "עימוד תחתון"
+ },
+ "nav": {
+ "systemPanel": "פאנל מערכת",
+ "policy": "מְדִינִיוּת",
+ "authentication": "אימות",
+ "networkServices": "שירותי רשת",
+ "server": "שרת",
+ "ariaLabel": "גלוֹבָּלִי"
+ },
+ "switchBtn": "Switch to English",
+ "table": {
+ "ariaLabel": "טבלת הדגמה מימין לשמאל",
+ "columns": {
+ "servers": "שרתים",
+ "status": "סטָטוּס",
+ "location": "מקום",
+ "modified": "עודכן לאחרונה",
+ "url": "כתובת אתר"
+ },
+ "rows": {
+ "node": "נקודת קצה",
+ "status": {
+ "stopped": "עצר",
+ "running": "רץ",
+ "down": "מטה",
+ "needsMaintenance": "זקוק לתחזוקה"
+ },
+ "locations": {
+ "raleigh": "Raleigh",
+ "boston": "Boston",
+ "atlanta": "Atlanta",
+ "sanFrancisco": "San Francisco"
+ },
+ "lastModified": {
+ "oneHr": "לפני שעה",
+ "threeHrs": "לפני 3 שעות",
+ "fiveHrs": "לפני 5 שעות",
+ "sevenMins": "לפני 7 דקות",
+ "fortyTwoMins": "לפני 42 דקות",
+ "twoDays": "לפני יומיים",
+ "oneMonth": "לפני חודש"
+ }
+ }
+ }
+}