From b63683488e4dc9f2db4c4242e8c22b52fb89c8f3 Mon Sep 17 00:00:00 2001 From: Parthiv Krishnan Date: Thu, 12 Jun 2025 09:10:24 -0400 Subject: [PATCH 01/17] feat(demo): completed initial version of animations demo --- package.json | 5 + .../src/demos/Animations/Animations.md | 37 + .../demos/Animations/examples/Animations.tsx | 1584 +++++++++++++++++ .../Animations/examples/ResourceTableData.jsx | 85 + .../motion/demo/animations.png | Bin 0 -> 74971 bytes .../patternfly-docs/patternfly-docs.config.js | 1 + yarn.lock | 371 +++- 7 files changed, 2081 insertions(+), 2 deletions(-) create mode 100644 packages/react-core/src/demos/Animations/Animations.md create mode 100644 packages/react-core/src/demos/Animations/examples/Animations.tsx create mode 100644 packages/react-core/src/demos/Animations/examples/ResourceTableData.jsx create mode 100644 packages/react-docs/patternfly-docs/generated/design-foundations/motion/demo/animations.png diff --git a/package.json b/package.json index 8a00c85d12b..ecae4943afe 100644 --- a/package.json +++ b/package.json @@ -120,5 +120,10 @@ "packages/*", "packages/react-integration/demo-app-ts" ] + }, + "dependencies": { + "@patternfly/react-component-groups": "^6.2.1", + "clsx": "^2.1.1", + "react-jss": "^10.10.0" } } diff --git a/packages/react-core/src/demos/Animations/Animations.md b/packages/react-core/src/demos/Animations/Animations.md new file mode 100644 index 00000000000..16826a29c58 --- /dev/null +++ b/packages/react-core/src/demos/Animations/Animations.md @@ -0,0 +1,37 @@ +--- +id: Motion +section: design-foundations +source: demo +--- + +import { Fragment, useRef, useState, useEffect } from 'react'; + +import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon'; +import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon'; +import BarsIcon from '@patternfly/react-icons/dist/js/icons/bars-icon'; +import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; +import PowerOffIcon from '@patternfly/react-icons/dist/esm/icons/power-off-icon'; +import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; +import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; +import imgAvatar from '@patternfly/react-core/src/components/assets/avatarImg.svg'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import pfLogo from '@patternfly/react-core/src/demos/assets/PF-HorizontalLogo-Color.svg'; +import MultiContentCard from "@patternfly/react-component-groups/dist/dynamic/MultiContentCard"; +import { ArrowRightIcon, LockIcon, PortIcon, CubeIcon, AutomationIcon, ExclamationCircleIcon, CheckCircleIcon, ExclamationTriangleIcon, HamburgerIcon} from '@patternfly/react-icons'; +import { createUseStyles } from 'react-jss'; +import clsx from 'clsx'; +import UnpluggedIcon from '@patternfly/react-icons/dist/esm/icons/unplugged-icon'; +import l_gallery_GridTemplateColumns_min from '@patternfly/react-tokens/dist/esm/l_gallery_GridTemplateColumns_min'; +import {applicationsData} from './examples/ResourceTableData.jsx'; +import SkeletonTable from "@patternfly/react-component-groups/dist/dynamic/SkeletonTable"; +import t_global_text_color_subtle from '@patternfly/react-tokens/dist/esm/t_global_text_color_subtle'; + + +## Demos + +This demonstration highlights PatternFly's latest animations. Explore how components like alerts, navigation, and forms use motion to provide clear feedback and improve usability across the platform. + +### Animations + +```js file="./examples/Animations.tsx" isFullscreen +``` \ No newline at end of file diff --git a/packages/react-core/src/demos/Animations/examples/Animations.tsx b/packages/react-core/src/demos/Animations/examples/Animations.tsx new file mode 100644 index 00000000000..6f03f327181 --- /dev/null +++ b/packages/react-core/src/demos/Animations/examples/Animations.tsx @@ -0,0 +1,1584 @@ +import { + Fragment, + useRef, + useState, + useEffect, + ReactNode, + FunctionComponent, + FormEvent, + RefObject, + MouseEvent, + TransitionEvent +} from 'react'; +import { + AlertGroup, + Alert, + Avatar, + Brand, + Button, + ButtonVariant, + Content, + Card, + CardHeader, + CardBody, + CardFooter, + CardTitle, + ContentVariants, + Divider, + Dropdown, + DropdownItem, + DropdownList, + DescriptionList, + DescriptionListGroup, + DescriptionListTerm, + DescriptionListDescription, + EmptyState, + EmptyStateActions, + EmptyStateBody, + EmptyStateFooter, + EmptyStateVariant, + Flex, + FlexItem, + Form, + FormGroup, + FormHelperText, + FormAlert, + FormGroupLabelHelp, + Gallery, + GalleryItem, + HelperText, + HelperTextItem, + Icon, + Label, + MenuToggle, + Masthead, + MastheadMain, + MastheadBrand, + MastheadToggle, + MastheadContent, + MastheadLogo, + Nav, + NavItem, + NavList, + NavExpandable, + NotificationBadge, + NotificationDrawer, + NotificationDrawerBody, + NotificationDrawerHeader, + NotificationDrawerList, + NotificationDrawerListItem, + NotificationDrawerListItemBody, + NotificationDrawerListItemHeader, + NotificationDrawerGroup, + Page, + PageSection, + PageSidebar, + PageSidebarBody, + PageToggleButton, + Select, + SelectList, + SelectOption, + Spinner, + SkipToContent, + Toolbar, + TextInput, + ToolbarItem, + ToolbarGroup, + ToolbarContent, + Tabs, + Tab, + TabTitleText, + Title, + Timestamp, + Popover, + ActionGroup, + Grid, + GridItem +} from '@patternfly/react-core'; +import { Table, Thead, Tbody, Tr, Th, Td, ExpandableRowContent } from '@patternfly/react-table'; +import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon'; +import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; +import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon'; +import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon'; +import PowerOffIcon from '@patternfly/react-icons/dist/esm/icons/power-off-icon'; +import PortIcon from '@patternfly/react-icons/dist/esm/icons/port-icon'; +import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon'; +import AutomationIcon from '@patternfly/react-icons/dist/esm/icons/automation-icon'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; +import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; +import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon'; +import imgAvatar from '@patternfly/react-core/src/components/assets/avatarImg.svg'; +import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; +import pfLogo from '@patternfly/react-core/src/demos/assets/pf-logo.PF-HorizontalLogo-Color.svg'; +import MultiContentCard from '@patternfly/react-component-groups/dist/dynamic/MultiContentCard'; +import { applicationsData } from './ResourceTableData'; +import SkeletonTable from '@patternfly/react-component-groups/dist/dynamic/SkeletonTable'; + +export const Animations: FunctionComponent = () => { + const drawerRef = useRef(null); + + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isKebabDropdownOpen, setIsKebabDropdownOpen] = useState(false); + const [isDrawerExpanded, setIsDrawerExpanded] = useState(false); + const [isAlertVisible, setIsAlertVisible] = useState(false); + const [showForm, setShowForm] = useState(false); + + interface UnreadMap { + [notificationId: string]: boolean; + } + + const [activeItem, setActiveItem] = useState(0); + const [activeGroup, setActiveGroup] = useState(null); + const [isUnreadMap, setIsUnreadMap] = useState({ + 'notification-1': true, + 'notification-2': true, + 'notification-3': false, + 'notification-4': false + }); + + const [shouldShowNotifications, setShouldShowNotifications] = useState(true); + + interface ActionsMenu { + [toggleId: string]: boolean; + } + + const handleShowForm = () => { + setShowForm(!showForm); + }; + + const [isActionsMenuOpen, setIsActionsMenuOpen] = useState({}); + + const [selectedTab, setSelectedTab] = useState(0); + + const [shouldNotify, setShouldNotify] = useState(false); + const prevUnreadCountRef = useRef(0); + + const getNumberUnread = () => { + if (!isUnreadMap) { + return 0; + } + return Object.values(isUnreadMap).filter(Boolean).length; + }; + + const onNavSelect = ( + _event: FormEvent, + selectedItem: { + groupId: number | string | null; + itemId: number | string; + to: string; + } + ) => { + setActiveItem(selectedItem.itemId); + setActiveGroup(selectedItem.groupId as string | null); + }; + + const onDropdownToggle = () => setIsDropdownOpen((prevState) => !prevState); + const onDropdownSelect = () => setIsDropdownOpen(false); + const onKebabDropdownToggle = () => setIsKebabDropdownOpen((prevState) => !prevState); + const onKebabDropdownSelect = () => setIsKebabDropdownOpen(false); + const onCloseNotificationDrawer = (_event: any) => setIsDrawerExpanded((prevState) => !prevState); + + useEffect(() => { + const timerId = setTimeout(() => { + setIsAlertVisible(true); + setIsUnreadMap((prevUnreadMap) => { + const newNotificationId = `notification-${Object.keys(prevUnreadMap || {}).length + 1}`; + + return { + ...prevUnreadMap, + [newNotificationId]: true + }; + }); + }, 3000); + + return () => { + clearTimeout(timerId); + }; + }, []); + + useEffect(() => { + const currentUnread = getNumberUnread(); + if (currentUnread > prevUnreadCountRef.current) { + setShouldNotify(true); + setTimeout(() => setShouldNotify(false), 1000); // Reset after animation + } + prevUnreadCountRef.current = currentUnread; + }, [isUnreadMap, getNumberUnread]); + + useEffect(() => { + prevUnreadCountRef.current = getNumberUnread(); + }, [getNumberUnread]); + + const onToggle = (id: string) => { + setIsActionsMenuOpen({ [id]: !isActionsMenuOpen[id] }); + }; + + const closeActionsMenu = () => setIsActionsMenuOpen({}); + + const onListItemClick = (id: string) => { + if (!isUnreadMap) { + return; + } + setIsUnreadMap({ ...isUnreadMap, [id]: false }); + }; + + const markAllRead = () => setIsUnreadMap(null); + + const showNotifications = (showNotifications: boolean) => { + setIsUnreadMap(null); + setShouldShowNotifications(showNotifications); + }; + + const focusDrawer = (_event: any) => { + if (drawerRef.current === null) { + return; + } + const firstTabbableItem = drawerRef.current.querySelector('a, button') as + | HTMLAnchorElement + | HTMLButtonElement + | null; + firstTabbableItem?.focus(); + }; + + const cards = [ + // Card 1: Performance + + + + + + + + + Upgrade your kernel version to remediate ntpd time sync issues, kernel panics, network instabilities and + issues with system performance + + + 378 systems + + + + + + + + + + {' '} + System reboot is not required + + + + + + + , + // Card 2: Stability + + + + + + + + + Adjust your networking configuration to get ahead of network performance degradations and packet losses. + + + 211 systems + + + + + + + + + + {' '} + System reboot is required + + + + + + + , + // Card 3: Availability + + + + + + + + + Fine tune your Oracle DB configuration to improve database performance and avoid process failure + + + 166 systems + + + + + + + + + + {' '} + System reboot is not required + + + + + + + + ]; + + const PageNav = ( + + ); + const kebabDropdownItems = ( + <> + + Settings + + + Help + + + ); + const userDropdownItems = ( + <> + My profile + User management + Logout + + ); + const headerToolbar = ( + + + + + + onCloseNotificationDrawer(event)} + aria-label="Notifications" + isExpanded={isDrawerExpanded} + count={getNumberUnread()} + shouldNotify={shouldNotify} + /> + + + + + + + + )} + + + ); + const EventsCard: FunctionComponent = () => { + const [isOpen, setIsOpen] = useState(false); + + const selectItems = ( + + + Success + + + Error + + + Warning + + + ); + + const toggle = (toggleRef) => ( + setIsOpen(!isOpen)} isExpanded={isOpen} variant="plainText"> + Status + + ); + + const headerActions = ( + + ); + + return ( + + + + + + Events + + + + + + + + + + + + + + + Readiness probe failed + + + + + Readiness probe failed: Get https://10.131.0.7:5000/healthz: dial tcp 10.131.0.7:5000: connect: + connection refused + + + + + + + + + + + + + + + Successful assignment + + + + + Successfully assigned default/example to ip-10-0-130-149.ec2.internal + + + + + + + + + + + + + Pulling image + + + + Pulling image "openshift/hello-openshift" + + + + + + + + + + + + + + Created container + + + + Created container hello-openshift + + + + + + + + + View all events + + + + ); + }; + + const CardStatus: FunctionComponent = () => { + const [drawerExpanded, setDrawerExpanded] = useState(false); + const handleDrawerToggleClick = () => { + setDrawerExpanded(!drawerExpanded); + }; + + const [rowsExpanded, setRowsExpanded] = useState([false, false, false]); + const handleToggleExpand = (_: any, rowIndex: number) => { + const newRowsExpanded = [...rowsExpanded]; + newRowsExpanded[rowIndex] = !rowsExpanded[rowIndex]; + setRowsExpanded(newRowsExpanded); + }; + + const header = ( + + + Status + + + ); + + const columns = ['Components', 'Response Rate']; + + const rows = [ + { + content: ['API Servers', '20%'], + child: ( + + ) + }, + { + content: ['Controller Managers', '100%'], + child: ( + + ) + }, + { + content: ['etcd', '91%'], + child: ( + + ) + } + ]; + + const popoverBodyContent = ( + <> +
+ Components of the Control Panel are responsible for maintaining and reconciling the state of the cluster. +
+ + + + + ))} + + + {rows.map((row, rowIndex) => { + const parentRow = ( + + + ))} + + ); + const childRow = row.child ? ( + + + + ) : null; + return ( + + {parentRow} + {childRow} + + ); + })} +
+ {columns.map((column, columnIndex) => ( + + {column} +
+ {row.content.map((cell, cellIndex) => ( + + {cell} +
+ {row.child} +
+ + ); + + const body = ( + + + + + + + + + + + Cluster + + + + + + + + + + + + + e.preventDefault()}> + Control Panel + + + + + + + + + + + + + + + Operators + + + 1 degraded + + + + + + + + + + + + + + Image Vulnerabilities + + + 0 vulnerabilities + + + + + + + ); + + const drawerTitle = ( + + + Notifications + + + + + + + + ); + + const drawer = ( + + + + + + + + This is a long description to show how the title will wrap if it is long and wraps to multiple lines. + + + + + + This is a warning notification description. + + + + + + + ); + + return ( + + {header} + {body} + + {drawer} + + ); + }; + + const detailStatusEvents = ( + + + + + + Details + + + + + + Cluster API Address + + https://api1.devcluster.openshift.com + + + + Cluster ID + 63b97ac1-b850-41d9-8820-239becde9e86 + + + Provide + AWS + + + OpenShift Version + 4.5.0.ci-2020-06-16-015028 + + + Update Channel + stable-4.5 + + + + + + View Settings + + + + + + + + + + + ); + + const expandableColumns = ['Applications', 'Server', 'Branch', 'Status']; + + interface Application { + name: string; + header: string; + branch: string; + status: string; + details?: ReactNode; + } + + const initialExpandedServerNames = ['Cost Management']; // Default to expanded + + const TableExpandCollapseAll: FunctionComponent = () => { + const [areAllExpanded, setAreAllExpanded] = useState(false); + const [collapseAllAriaLabel, setCollapseAllAriaLabel] = useState('Expand all'); + const [expandedAppNames, setExpandedAppNames] = useState(initialExpandedServerNames); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => setLoading(false), 2000); + return () => clearTimeout(timer); + }, []); + + useEffect(() => { + const allExpanded = expandedAppNames.length === applicationsData.length; + setAreAllExpanded(allExpanded); + setCollapseAllAriaLabel(allExpanded ? 'Collapse all' : 'Expand all'); + }, [expandedAppNames]); + + const setAppExpanded = (app: Application, isExpanding: boolean) => { + const others = expandedAppNames.filter((n) => n !== app.name); + setExpandedAppNames(isExpanding ? [...others, app.name] : others); + }; + + const isAppExpanded = (app: Application) => expandedAppNames.includes(app.name); + + const onCollapseAll = (_event: any, _rowIndex: number, isOpen: boolean) => { + setExpandedAppNames(isOpen ? applicationsData.map((app) => app.name) : []); + }; + + return ( + + {loading ? ( + + ) : ( + + + + + ))} + + + + {applicationsData.map((app, idx) => ( + + + + + + + + {app.details && isAppExpanded(app) && ( + + + + )} + + ))} +
+ {expandableColumns.map((column) => ( + {column}
setAppExpanded(app, !isAppExpanded(app)) + } + : undefined + } + /> + {app.name}{app.header}{app.branch} + {app.status === 'Running' && } + {app.status === 'Degraded' && } + {app.status === 'Stopped' && } + {app.status !== 'Running' && app.status !== 'Degraded' && app.status !== 'Stopped' && app.status} +
+ + {app.details} +
+ )} +
+ ); + }; + + const EmptyStateNoMatchFound: FunctionComponent = () => ( + + No results match the filter criteria. Clear all filters and try again. + + + + + + + ); + + const CreateDatabaseForm: FunctionComponent = () => { + const [name, setName] = useState(''); + const [isNameValid, setIsNameValid] = useState('default'); + const [email, setEmail] = useState(''); + const [version, setVersion] = useState(''); + const [isTimeZoneOpen, setIsTimeZoneOpen] = useState(false); + const [selectedTimeZone, setSelectedTimeZone] = useState(''); + const [password, setPassword] = useState(''); + const [isPasswordValid, setIsPasswordValid] = useState('default'); + const [isSuccess, setIsSuccess] = useState(false); + const [actionCompleted, setActionCompleted] = useState(false); + + const labelHelpRef = useRef(null); + + const handleNameChange = (_event, name: string) => { + setName(name); + setIsNameValid(name.length > 0 && /^[a-z0-9-]+$/.test(name) ? 'success' : 'error'); + }; + + const handleEmailChange = (_event, email: string) => { + setEmail(email); + }; + + const handleVersionChange = (_event, version: string) => { + setVersion(version); + }; + + const handlePasswordChange = (_event, password: string) => { + setPassword(password); + setIsPasswordValid( + password.length > 12 && /[0-9]/.test(password) && /[A-Z]/.test(password) ? 'success' : 'error' + ); + }; + + const onTimeZoneSelect = (_event, selection) => { + setSelectedTimeZone(selection); + setIsTimeZoneOpen(false); + }; + + const timeZoneToggle = (toggleRef) => ( + setIsTimeZoneOpen(!isTimeZoneOpen)} isExpanded={isTimeZoneOpen}> + {selectedTimeZone || 'Select time zone'} + + ); + + const handleSubmit = () => { + setActionCompleted(true); + if ( + isPasswordValid === 'success' && + isNameValid === 'success' && + selectedTimeZone.length > 0 && + email.includes('@') && + version.length > 0 + ) { + setIsSuccess(true); + setTimeout(() => { + setActionCompleted(false); + setIsSuccess(false); + }, 4000); + } else { + setIsSuccess(false); + setTimeout(() => { + setActionCompleted(false); + setIsSuccess(false); + }, 4000); + } + }; + + return ( +
+ {actionCompleted && + (isSuccess ? ( + + + + + + ) : ( + + + + + + ))} + The name of your database} + bodyContent={ +
+

+ The name of your database is used to identify it in the system. It must be unique and cannot be + changed later. +

+
+ } + > + + + } + isRequired + fieldId="simple-form-name-01" + > + + + + : } + variant={isNameValid as 'success' | 'warning' | 'error' | 'default'} + > + Must be unique. Can only contain letters, numbers, and hyphens. + + + +
+ + + + + + + + + + + + + + : } + variant={isPasswordValid as 'success' | 'warning' | 'error' | 'default'} + > + Password must be at least 12 characters and include one uppercase letter and one number. + + + + + + + + +
+ ); + }; + + return ( + + | KeyboardEvent | TransitionEvent + ) => focusDrawer(event)} + isNotificationDrawerExpanded={isDrawerExpanded} + skipToContent={PageSkipToContent} + // breadcrumb={PageBreadcrumb} + mainContainerId={pageId} + > + + {isAlertVisible && ( + + + Something wicked this way comes + + + )} + Resources + Everything you need to know about your application + setSelectedTab(Number(key))} aria-label="Primary tabs"> + Overview} /> + Resources} /> + Database} /> + + + {selectedTab === 0 && ( + + + + + {detailStatusEvents} + + )} + + {selectedTab === 1 && ( + + + + )} + + {selectedTab === 2 && ( + {showForm ? : } + )} + + + ); +}; diff --git a/packages/react-core/src/demos/Animations/examples/ResourceTableData.jsx b/packages/react-core/src/demos/Animations/examples/ResourceTableData.jsx new file mode 100644 index 00000000000..ca43cd7003d --- /dev/null +++ b/packages/react-core/src/demos/Animations/examples/ResourceTableData.jsx @@ -0,0 +1,85 @@ +import React from 'react'; + +export const applicationsData = [ + { + name: 'Cost Management', + header: 'US-East-1', + branch: 'main', + status: 'Running', + details:

Monitors cloud spending across all services. Last updated 1 hour ago.

+ }, + { + name: 'User Authentication', + header: 'EU-West-2', + branch: 'dev', + status: 'Degraded', + details: ( +
+

OAuth 2.0 provider is reporting high latency. SAML provider is operational.

+
+ ) + }, + { + name: 'Data Processing', + header: 'US-West-1', + branch: 'feature-new-parser', + status: 'Stopped', + details:

Stopped pending deployment of new data schema. Do not restart manually.

+ }, + { + name: 'Inventory API', + header: 'US-East-1', + branch: 'main', + status: 'Running', + details:

Provides read/write access to the product inventory database.

+ }, + { + name: 'Frontend Web App', + header: 'APAC-Tokyo', + branch: 'release-v2.5.1', + status: 'Degraded', + details:

A new vulnerability (CVE-2025-12345) was detected in a dependency.

+ }, + { + name: 'Logging Service', + header: 'EU-Central-1', + branch: 'hotfix-log-rotation', + status: 'Running', + details:

Aggregating logs from all production services. Current volume: 2,500 logs/min.

+ }, + { + name: 'API Gateway', + header: 'US-East-1', + branch: 'main', + status: 'Degraded', + details:

High latency detected on the `/v2/query` endpoint. Please investigate.

+ }, + { + name: 'Notification Queue', + header: 'US-West-2', + branch: 'dev', + status: 'Running', + details:

Currently processing a backlog of 1,500 messages. Estimated time to clear: 15 minutes.

+ }, + { + name: 'Billing Processor', + header: 'EU-West-1', + branch: 'main', + status: 'Stopped', + details:

Service is stopped pending end-of-month financial reconciliation.

+ }, + { + name: 'Content Delivery Network', + header: 'Global', + branch: 'config-update', + status: 'Running', + details:

Serving assets globally with 58 points of presence. Cache hit ratio: 98.2%.

+ }, + { + name: 'Reporting Dashboard', + header: 'US-East-2', + branch: 'feature-new-charts', + status: 'Degraded', + details:

Data source `reporting-db-replica` is out of sync. Reports may be stale.

+ } +]; \ No newline at end of file diff --git a/packages/react-docs/patternfly-docs/generated/design-foundations/motion/demo/animations.png b/packages/react-docs/patternfly-docs/generated/design-foundations/motion/demo/animations.png new file mode 100644 index 0000000000000000000000000000000000000000..c47c5e5ab1aa27b072952a0f983ecdbe5b267016 GIT binary patch literal 74971 zcmce;byU=C)CG!&fV8x9Ntd*wA}s=vBT@na0s_*Fw3LX5h?IzkNDUy}AR^t}Aky7^ z&+vZVUF)uU*Shzw%UUn*$jtoa`8{#YK6~#oVH#?R_&8KJXlQ8o%1UyYXlPd%(9o_l zV57rdK3)+|h5uc5P7HBC2yC`JvdcAGUjzXEhTLZ<8$TBdq?ZmR!AlsFIJdi(MEi$AY6Vmnc=!F@?i2h zIT{96ozWGdYF^LBtEY4(wis6;zAMDco_Orn7a`g^@m!7Dy|+47C44<$Ebud3G3(}2 zboIY~+%>s!`M|*Pe;?W2^Oq;`Odk3+P%hd~y;LO{F#Kv1^_n^!e>Fv`JP+p28xvp9 zBN%oq(kCq(U%19j+6`(A`i;KTiRQ+5#`t!^gnxW-`SKmPuhbNWc`*O-8}(U;QsMDV zH9rhuz;4vNs&~SQ%N=MW!z_r0gMU@_er;X1p#68+zJWJO*1umb-~M79`TZ}6j@j`t zhZdtAhn97eLOko`kWKKFvi+rUTNm|Pm&h%R2NW#BaXoJd#C2MTjLStmLfxJH}lVw z3umxykMhwa9>pItXMXj}USfUl9Ca7N?TgMB&u{+{jgT&_o!W5OUof&fF&x-G8XED< zFQCV!B58H`m$)$TRwaWtOs*sYp|7u*Jzx`EIpIJ^Zr1R8Ul()eqsoGH`~ZeXg>+TcJ}V(?#_%uQgY`2G+&nyn z>VzNEGW$GVKE5NnxOdCjjA|&OfOqd^V8ZX6T+qW4E!Um8%s4H8&!7v@=vR ztH~P2YbzJ0jCoV{rq-OW%DL#8>5piR8lL7&QLh>n#Qu5Oo5O;1sP%IZ_eT_I{YHPl zLVSmYfE(#ury25vJ+m)tN;B~){EtYJom3^G*Gk9;3wB}9gx!Iz19`-}K)iZ>KBb@q z9+g)4Z?RGmuAgIvnlbz57)J-4s+H@z=f1UpXQkCrhY=WiXF_L@+V(HKIIaiqC#X(( z4w@D$j#~*)c^A2e(<;9skGK{c!W4vu8Mwe4r4YChVqc_?zBTSrl|QsXOAwlgo!R$X zByqsHSS4foHBa;_)TVpY!Hhiq_TJZT&6xM2wA<^G@~daQ_DC7Gy5b9UdfLT%nA@hvMcW&izj!Q3wU_yOjsr_jiR?H>brY|A-(I1reBC8 zs-s5_Zm}byuEe`cV36MBUb&Y4kM?u?m z8i8-Sl^D88r3EIxHqTsJ%GrOqTFFTQRQPAGIMPBda~?un!3hy&-$ZC6TOFIYBTN z0&`XOZ`G+*2>blUbBa7h<~zeC4q{T&r~6FYHotAvcFd#>Z(=yXf5-=eB;C7W9-2 zCUMB-=cft?C`9ctnlUwN^IOxzF2*zx8L@4SHJ;Bt9pbqd#i|PJ6|Pktko5f8rK~0S`EKNztpk1k`kkI(cW?H_gP4w6nRaMp9 zogJ@(3AfOY5F%L~9-gqUFxj+jiy~oR;o_e^zkdCShleL5BJ$z=dlpvKH<6JHLB9J% zsS`d26Y;!;;!gimy}c!)6i$})YV{vKer#ySLmts#;q>Cg3pcleXS6P;Gw0xR?SqY< z-^H3A{c?gZL(mV!pT?4s({9s6PwTIaN4Lf^B|cM(kmHpZOe@A9cg=!9C5gK%*PN{H zz}SxF<6~oEJ1lmrs~KGdo(p|ar1SVYT_1S!=1oGW-R_oHHjZ%p4&0)UIwob}2+lYa z7`d>tG;eH6m6SDN_RqZI_>#JsT4zUx$8VzQXWiE4Y#XI|%V!nmblkBmLc+oW@G~|x z*4Ni)E-=K5q{@uCmi~>n=H=bRPvhpdD`q1!Ma=H*eVL+8>Z)!!diMVC>8F47+5>N# zJ8~i&aJhM8zax>GHpVCVT3WFNycc`*%S?QGTiQmP6 zUuI_Jl-EYJ%wauY&B?^Z`EI66oJxkoM1RFSY}nR58(vcnR8>z;7i5fK{)w<_HiP+o z{rbfalxKy33d?9B2os-~%~w-&`w|}L@h8nQjuore$-851M-7be6~_%@S1%l2p9CIY z^;O3CVtI#R`(TjMoj$j-+uzw4E3#Dg@Hl!=I0yje&3lQ9<1!bkBdhDcy z2=2-Wy8^_pZHZdu&fpXAWIGcR6Gz8)&2vZno8q|K$!eBGBl`OKaPTRX|BaVB(GV;Y z-Vegp`kituP;=#-?yvy%5SRbz@AtYheD^KrRCJ_1=D0IFKGOKns%SFdI`+bC(qnB;wbgu!-uS~9K5`|tgQcW8iGwmyC*h$z18tW~c@Q&-obXru<# zcx|{~?r>|)*VotI-@i-xWEEZ?MD!^oShluDQIazygQX3i+G`zMi zv0gUUopwK-4AIlmV|+SG0ReKu^4T-^)Y{`tx0=R<9lgHmHrU*f>7N*|{qOSswQzMM zC!{dl3cc02xM-!OMj-pWpr8PTe|ogz;^IO>fR+8ewN=H>&yP3WYiqXY{rmT`v!+?v zRK&y$Cj9SWWBYo0PtVR^ZL@#;pu|&fcXx-yAACXQzxRtoeH>&*AO`8aoTNduET9J^1{aO!#Z|i}Qtr zgdkS@F&7Bx4kKZM+k3OPxVYhWagtxXPbIsETC%OV<{Pqsfq@st`4^w!;+_dnZB!pj zPMxj0LAF}xOg0lp@H=dzpY%B}4C;nM<5qhz(c7z;-gB61?~@I;+M`JCvs%z>E0Uk(xN+a7#)b`khM)G4O{8mE10PtT5b$S-j5H zGEPWV)6%l~NWS=wuVaYc*jQd#3O?lo3Bjr&Er3bM3t{5%?I#}(&y1ZoMCK;bmtsl7!^3dz zZ{NNh92~5usDNb%V6YPUWWXC85zz)~^zb46_3JIIt;9INLR2q&eWj(O&KiPouU)$a z(K2OeU(C<{uEY7MM_|41-({&t_cJL3L1_AyFSGqwT376(j~COwefw4{h48F?pkMy? zy}Dh{q(%RbDrKh|RioEKf5SZfuk^)K@$Y$szrDoG2?z+_^I3Y}ElnTY{fLwdZYO!e zdm&16^6;N%?a_7{A9IkA&)xtZL=ZRtdHT7&oAp?=+j03pp`kuf z5!@e=CEV!59MGbBCLwPH2M6ot?yneMcs_r=#2hWTJ!}^Jh%`7uG_d|~JHA#@NJx(8 zm(;=d4!qyX*?G6nmErR{T0bP%)I za#1d{7y6oGgMlFXp<^>iJ`ec=6O-vTv=ZMkGq)O_+PLc=)vWC7e79TJQhoME^o)I6 zUcT(HE;eeS;XaN0q>=$gJtZkA>ElOAdU}d5xiV+_xIW10q@>#V`pXVtDi0pSzI*rK z!v`E(+?#kIN-WE3Yr+Ytw6wIGaX-@2(@RQ3AoiuD`L~$2&Tr@C@rKFu_VqCnVVjzn z-Jy9Co!Cl3J^Q<}z5&2tQo3fAHaqD`LV{dQq4-SI_}gLo$fq}%Jq@GDjTVKD-6-R= zdHpk-x()#1udS^Shha>KR2$hBAjBmk;52%Ac_}I=Y*M~3Gc!|Hj~=il3zLhBi-S>M zr^Up?!2Xd=wdbb{2?95T6%0?0AXE~Rf~lf9}jQ$;D8zr^VB@+E`yc5 zeSF(|a`NR~=E|=MC>|m+WJ1k*8Hf6fijvy>Dc6rD)g;WRZW|YUiuTB}eWYPN*3c~^ z60JdvdG&S0s_o6yV*yG$TwGj{MEqtweV#2U^tKrXtK!kDtgM-tnV7EWEqBLqZx1A< z{lpvzuT7JXP3nr1LlYX2sGP}7Jj`?oJ=)klt8Tq0>USqm90q!C557yAeUy;ECPUm|#v$eG){8bcnXSNk$h&*?_bu|38hH&2*q|Hv)z3^*; zc|l|t9~MUHxb**Fc-?BZqv#){T^xN%)o!t~xX50V=>|d2+4*O5lSR=Tcj@TT*)2KE z%^zxVdvRqbiwL{44uV7WH7a4MsMD?MlG0+Wa}ghY zC7R3As=s}`!hy`YA)7C{xQ7!}V>(Vo!{zsjjGoJX8{LW z8W5vqNQ4k$W?N24yP{GZ?_}+1We7T%Aw}`_4mU1_)TO6G~ z3}sPHR%44%8cbm?PD95v6DngZpX-<{zG+{wfg(oGN-)F&kWcUgir?^cN8;u?Qj=I& z{GzIC${?}Ry|8hx;qK7|jApc-J%QoB_n0Q<+1SIZ%QxEIbH;cowD`iJvDlV8_Fi^n z%YBg$KVM(FY^3rva@3{KhOqCI{i`ybE4f)k{fqo>p)n4|e^1o8VE#Wo8hmJS#$0y! zUR%t(pL3Z0#XZyA*1Fkb&Apd@HQ5P_UlOFmgVK!wDMA{+uqhrUDnY<=`J^76{3G|J zmA3suc#fcIdpB z(%B9>piV+OSuIz0j#*!nPWW5jlBC3;R#i^4ZuWqc(D4gy_!u@6jCna#-q>;~+NimL*IJ-XH!&>D{je+zt4rV14Q=^@eho%olbXiQkfnn{N)Dc$4#@ z02$gVd}0J<;JxetNRAff=6~nsp9@iC4_KFuMJc2=ncl;GsICt67DLc`m1pwu*HFd0 zeE!g!y!WoBlp2Ir8iG*B-r~tP!Cwjsd##I2ua)3mBYzX^a=g0)h+p*f?b|{^yXPkx ze)}VqQl;IN%_r-g8~JN~+ef4kKU`8S2T>PAj6mG_V_x|6D@$w(q#AuABWY1lJ*Wj@ zl#WkMsHv&%(5L}WGBliMXpmJ^#S5(e`0*pGAruc$QBjzIX_b|g#l<>V+JpW54q{35 z_4l}kl-`Y(pyPT)c0!eVxcSuE+|%=zdg?I5fOlhY@vtxTd>0_x&(c!npfZDXcufjq z#JT3^l|9eO316twp-MB~n5>pfBVu&k{@dd3eV%=jR8tetQFYkGaWZ**Q6vQK0K=Z|EBO6kBEWn~}LGASr14)*s2C?i8cFfjvT5)<1NcAOj?!^6Wrs#*Y= zfAWMnq6z+GZELHlth}_e)Y8^QO+y14jUd!45jUrdMn0P%^xHf*puhMU%lKl? z_}Q~(*lD}FyHHA8+@z+iTq_xqK0lm0Iyy3IesczB#rFC0J09ESuCA`8rn7;B(qiJ` zo~y$JhLx`P1OzDbZ@^ocn~VQnSg`$B_pWXJ#*G`^(lWPi7XVIBNQY0z$jI0_I_>Z8 z2lV~#-#-|8PEHQ|Gc7F*ut;xjZ%IkX@v*0Path@24*&>Xf?Be{S_Mm!$F$i$aJ}k44q@*MZ%ipo7bJQ3|iYzy3FU|?24?pMT zKIDw^y*QalO-QJBJGPWLf$D!kOI=FG&@hUqwX<^)ikQvK&9j54XhkzX-A1gjOE+n} z7t=yfd*#ocKQIXsfPZy#bO6Tv$uq1(M@Rp+uuva(83L%qTap=G!yOeC3Wy}WC>j~I zb@lO)if*2B5QAL{)nZ>?A90wut!)l$->+X`gN~XD4D|QIrnD&@-QV8_D7>`voG#iD zRWQ1~e7Q9}{dvT;COA+yr#WzbW24r7N;;?;wj-hM=2g0A3Q|&MSl`4lbs#XlH2@JjUFoA)F))_}HX z?Bq%82q8M2*zi3VwaG6SSsfhIWsS}1vBC}jlrHAEyQuEFO;T+{NOgIFMRqx4PpuLs z?AWjyEQ+3Dg$-CYNmvx!?D8lbTLZ9SD>A3N?rsMm&P|L4icQmk>AqyO%mM4_;DG^g zwsOD2nb0oD9Xx5rzkgzYIlIeFI_YE;<`5GT`yRu}1ADOwleCcHrOHYo%g)2ImRGrsgNKKr zGyunbN=!tRQiq3!eVOV|M#Ax08!gU;k^$-pLYY&3@~8Yew5TX4qGs~ESbqbFD>fa# zc8>m=q_|!}HBen3M^f_fiyeM^fTzQaK}4{*>+9auXIK6+Ab7 zP2MO%4IZsb^RCH@j0{J~JXUoLjjrQ6DR2*9?5P)f{&LK zx<2VMUt~XNc3{?%*;TzT&Xq%i-+-%J4nm4>F#Lg-R|J~m(S(z?$L#zAx%@N47q?lm zh@Gzn1>^Hf7B7U06kh0^1hg6JWh2!~xCWh?a0?ZDP~)h>B0@a5YcjL${q!kR82fvB zGp}{&VhRi}rb}6r)zsC!I)xKE=tPGduNRuyNxN*aZ2o=amZY`-0n9O!+*gO4^udD% zlkb``KhGOqVML`G&bU(3CmqWFm?s989_*+o5dCUs2+&~v)uR=k8LyI2CXoH#44`I1 z0G*40;q#9lKeDovb9K$mg6vb8OzT(1DDg1Ix5n1?;TSeGH37|#tQJZ{MMDFH1|XaW zV0S}?D(dSgB)=%;4?W2poG0sE*>kO=Wi%I}D$pp>&n;^pmFFN0fBhOei6>cYXl0L7 ze#u#!hVu&q&U|01(yHzkqq=05(GF$&-~E<{08XRb$nAOmD>R z5e2DtY!RblGX_$7D)02u%&_pB=Kx5wT>1h)!t-je=rZ>6ae5^GL zlQWk<%Y+%@1DnB#u+p2gCQppPrU(if8=Ds*PvFG;{c8?02Vu_9(Gjxf;h{T_RtgH3 zi5>qO%WvJf6-q$+BQK8tAHUjt%M^|g@F^Z19_Hrea&kd(Oi;ArT)$o)K@6d>tD{2) z^3mWR6lHx&yEQd6z(4?-1C?WthLO=Ed@4{A@gF`w#YjR-EXQ;sS&a}EH}TUan0OpQ zLK&Z9Az3AM()Gz|>UfCyy~D$=iI&&bVfF28ZEfxB2nY$I!^7cdKX~{sDmvQU+S*hg z0hrd@++2iO*1$6C1xTMTY0)z0V$#w^+~klSv9Pe*TwQf_bwLP$mhNtPH=f;lSkbIXP7|HC7gupx|I(5s{M8 zQn+TH)z9^HJIp}1KR`jGR8;!f+AQ&Hf&v1qt*yY9#xN^>RQ)$Rj8Ilqc5vX2Xfoxe zeERfhLP7$+E;ej<+wZ6nZdkscL->OwOmrjs2&kB`hK$RwV-;A0I9S8N$cM2ksd=Ehq@10HK=McmMu#z4eR8JUyAU1k1OHyb`G>hsC-|(dT;5DZZH^wXzL7M7NQ z0Rf|NIXO7^+k~HZV)_(t z;@HrTsJJ-Q&6^3IKD93FG&VNE#Q+i$$=ZGdM^X}^C?lXS zNO!<81Brs2Ha({g_7zNqI-J($&pBW< zxVTnUSMjc2e{N;Q$giVEb^BbIHiaz^xV(+~(s`&(#HhI;=`a-#^VH3N=n_c4 zQ`asWu_cwC!N3V_5Tpie;th*(_CQ5h*=tMP85K@aoX|!_B5XBmoN7Wi6UbRr0 z@%G2IJH-*nQbj2MRKg2^y>WEp5v1|x(IXd^VnAnPb$SX+G3=x`;a@P&uhftN(ulVb zNPx%kj}8N-hoX*?QH9Fb6O)@!3greVZ{L2_<^(nz$Tu!dP9Or0ynTV-x3tVeaCiRs zW5Q1flMhD@W(*1q;5gmir~%yG+;n*IM#6WfNf_yn9t&sh-&;6(`ToCLtoaHyoBn{sLIK8<>dCC#CS zTZYsh&nga-;5POibwbwG(%Yny#U9;#_f?g(@%np}jA-0eHibs(DW53ZONc7k8knKY z$;`w=OhN+p0nvMIZf<9L8{!6todCc=s%vg;J~=soyaR_9KGSg`GCDf*+c%~lU=h); zUIiN3(aGt-ICijXY|BrwYr6LK8?gP|B~u2h4adTY>HOzOMoo_Hb;Xcf70ab$URG%| z$o()~NcGG^BZSOMC`ZO|V4EsWH=~%V{1f?Z-a@!F@p}|?f&`kvTtGRaH!>10wDDeM zA5^>mP^qY?x8^K>u|?5RQ1OKCEt2l2)y8oR4@a=&d;y5Z>j1xV}nZoGgS^-M&Ht}`;PH0N4CCsKhJGm z^TQy}hO1)7UBr{`GVoJHLAvLRvxXllE2T6ID5=ZK?<97JKq)06Vo^EJ!jxdF+%vVoJ{c z5!9n?uKV?p6+W-SqVc{3c|?<4DRq>BBsx&Tv;q z1_)aWL9o35SAcNQI74~!W{gS(L~cnsb#)A-(F zCC3y4oV_<#v__!pn2_yF5ZM5|Cz>wU-2B*gM(J8PG zP~4Uq_yWgte(vjk7hY*wfLL_)1QEBtpH%rBU>F!~dJo9{o$c*#A8?hzKL(&5|NSV7#pi+_JMi;Lk5iuG66)g%8Cj=yrBC5?gEH)_wHSo zO9uxB5aXBnG8-W?x<2+g#Pe&N+XB1_Z-%=@a&f`3fc7CO>gMPuz{h9*@?`H>E|1e1XB;XjDmB2m&_#7~Ouq>o{#$+{E zE)+22;mOU<2Vl<4!NI}FIqM(>a|GiOPtN=HEkq$5pa>5)H_#m*8tZd^D4r+abu%?( z5*Bs=rc7EorFrg|xp}wMPe304zb}>BpD--Yb}$2P0}ce>rK1yz4!_BP%(GsG3TU*;hU;@6Ur-K`(a>D4*qxyNrCetmbg=DQv zOzuKJWg$*bEh#HQXlMZEzOb+$&(!nh&ujhMnj9@GzaQG1w}`LHKk}9~PSb!|Jxq=% zO#@OZdwkn1Vr5{mv^k5%)}R1_rDBS)dj9;n#27%gb_*wCWBT|u;3okjNz&OT8~2&; z!}~%0gcu79~C4kwm zWr5jnrQzPVi^cZ!Ou63K1uh` zS55KOqD$ZA$eU$C`LgQbAiim##f+n9hiP3X{|AW>q`@?5*36>4*KhP*{+Sa+7YrxS zac_KFg_ohB9U@)nabm_ri|ZOn)y_$%Q#WnxpCX5dUxlJmUS?Cr1bhtS=c1|t}a+9`r z#1?FSxw>trSECIR{dFu?NC$Bih+Qd^$U(ki0<=Je=JNssa2lSym)=YVLkIC>)>vh* zEZ~K-S%Wn3MA;-uo3q-;&!_#;FZe23HdmJfqu^2wxrRYrWHI8 zT>}&$`tUE>oC8!lY4=QCxcUcY6x5rPtvc{sK1q$X9x-6ON0lA~+b;x#s#C5bM0+hR zpDe>GAB=XUBjc;L8Ytcs6VYJT;SQsnZg@Pyk!0BMmcSf zbzj(*$-Nj}u00P~k({u+b$P3Z8$^gPHIt|Fe?6*melRHaLbU^AOr!rTildt;Q5@!sB-i(;*pAMU_Zqxc#GJ zoc9^`YWCV{o6j~m{%TXIo8U$4TyVHiZy~k}uu&!O3v8gnT+ayTs zQpARH4Hc>B9hVXPdUqzcd`WNsW5XIVQc{%qLD2E>F~s0`Vw?w|P;Uof5&qEr0l5m2 z2b5L7nM2YEB8qL{W@l&T*sF8yrcX4TJUK z?OTR8m6zvGih_Ly95J5N(jZ0T=MxU)LK41n=P_&S$lCrT{oxq&R*8iZ>M(V6V*>+j zPEMeF6tD|MY{ip}w6vg-rKYE^MIv7j!6!vWN2h-J^o9LLQIQ}Q7pV0(@x$HSZ&Ooi zofbMrMjjimhc?#7G(uGh<^2WN!C+^j>_|`|bhNfM+-S14vEehS0)j?bKw!Jg_*?)6 z3m&Ep(W&;%&JC$NRgC^4c0|S{fSNiS{)f z0H>zT_eW|Tceig1v&+hEZhc=LMWrvh%GBNdPE!& z$sg1`7cB!t_p0j&xHWo5M=SnAEP|Guz`&g?sr!BDTLKQgjsU5ZsRo%AO38~=OPRJm zf861gx{@VIK|7&~4kh}OkT6ngL-B@o8-$KqEG$V9ZUl_GVBd*hX#uhjc#w3(Xcd-s zphE#=NXh8D*q!$3)hkd3StKM*y8O(=A-SDwAUUD1|wcy~I#Azvh@Tw&j-uYcL++38GP+Ugl0?rmWIXQ^WsbVi>L5%_h6~y*Bvrnq5JK%R*1}~8p zIt^q12b;T)l0ss~w*>97U3Lz|Vq4n({P~`fV{2^cd?@)BdfN4Q^SE5Pth8HxHX}~q70nga}b_^$P~MPI$*W+M{e#!t=|P| zXY^Qo)m%!uA9@bjtFyB+9v)s598|A^b+DblP69C*I}Jo^iR4b8EUK!i08PfJfH^oK za|R0&6Y>RY8kk~C3=FUp8;n6+1T<>3(FJ(*_P~xA-N}3;rR6jGq`(6%-8IgmUOpNdXp>oMJ*XAFkWLoS=vxWV1jPTbv6i2m$0vUS zi7MQ;W+6as(B{lo*P_-3xd#>dv*E+L=_z9BlTSr}7XcL&+}L60^I^F2w5B+g!E>@6 zq#<8^_OE>M1FRWglolh40O+swz?oQeD>!7nkqAXrL zEhWdh7yQIfj-+WUhUFG_)|sIoLGG1nbUC@`+;K(ouaFz?moaiUi@HyDuydZc|7??R zUKTF=&5eSh(;q^?qoOk``5)?eRSdxS+5hNlPp$!$!~BoBKoS>+}_54fVVNZSV~uu1}a;4cK>ha<6=& zR5R-*g4^cnZAv^F=QCewcEJ+9A$gM-bz7DUkw+?XQ4`MO;m+sj8fXaoqGV0OqUsyu ztxF|BNYUx#x({;>Oi@S2rJ?$Q^RM_j0=`!sR zUS)E3y~?6cV1V9V{!w11t@47wGqI*6=~)BcNu5NBB{?k|$46IR;2_b-C4`w6g;{Og zrT0S~-gvUKtuBRmx)c&Ep2UkwQO-jnMDCEG%9)h+yY-p)UrX}E3A$s-WyeH=9_fph zZ@+XWJ+bxovAaxg_e^H47ICua>VZ{bRB7m+a|NbuQojrp<~wI`rFYJn1feT&EXtvQ zr@OCDn44wZh)rN^DdsVEs!vsHyQQwRx@g{O#qa)xRuieB+%PMESJc!yIeFv8%dF@o z$3*;w4}nn$U)XKccz0y~6wQ4Ki!|U!e&4oi@L|OiL^i`Rn9eFGAO~#&a`Pv~5!-qwUIahbDR}nvs^;)6Jv2 z8UWwJu-caIG7}Sv(qN|n;%aYKJ$Rzm=1&Arke8bqDi-NGz&y0fZ|_Mam1#B^)6z}fz8W0!NxLkk!9<8w+<9@fDFWTUZffcR4Bgm37z}~1@c1zYc?8HjB~syE zP~_7`ff%jv11z5VIfl?Z&cmbD@6-bRur@ zw!U51c5y1fW2)+nm1SkaqoY51tj7HwwxHng+X*`Wrpt?q(>q6)qLY>*O7U~#>+P>N6?WX^)y%nKpyqk2!Iq@*Z#4S=i0PJ@sQS`sj1fVd&pamKZRo(5UML4%~G z2DJicUqE+2VgTp}tf!=*0XhjxsB>HH825m_Hanq@d*?2D8_ggO*SXrdz5o4o-;KHl z@w`C8T*JbmxXf|TUBVRf%XBMCD-(bK$h1(~{rRH=W~1ol#tUs=n4pk=bjZZSM2Q!I z;HIOZ0@jKKes^?$_VzI;Njpnhl8!L`4lf(Nh!vr|4+_)CnS@wVl0gN5|O^ePIAS1N2w6wSX-8ljd2V@}7|3F3nPzCb@(+
    IxNh3;2_Oc*JywwGuu+yJ5Y7N; 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 N0X=16.8.6" + checksum: 10c0/38234c362fb21cac318e2be83840c8dd5a75a5689ee96d732997cc88c7775032d51d6186232a022c8b74c80c8d1ab943b0f984831a0e9c46357c75da919c66a6 + languageName: node + linkType: hard + "react-lifecycles-compat@npm:^3.0.4": version: 3.0.4 resolution: "react-lifecycles-compat@npm:3.0.4" @@ -19783,6 +20115,13 @@ __metadata: languageName: node linkType: hard +"shallow-equal@npm:^1.2.0": + version: 1.2.1 + resolution: "shallow-equal@npm:1.2.1" + checksum: 10c0/51e03abadd97c9ebe590547d92db9148446962a3f23a3a0fb1ba2fccab80af881eef0ff1f8ccefd3f066c0bc5a4c8ca53706194813b95c8835fa66448a843a26 + languageName: node + linkType: hard + "sharp@npm:0.32.6": version: 0.32.6 resolution: "sharp@npm:0.32.6" @@ -20848,6 +21187,13 @@ __metadata: languageName: node linkType: hard +"symbol-observable@npm:^1.2.0": + version: 1.2.0 + resolution: "symbol-observable@npm:1.2.0" + checksum: 10c0/009fee50798ef80ed4b8195048288f108b03de162db07493f2e1fd993b33fafa72d659e832b584da5a2427daa78e5a738fb2a9ab027ee9454252e0bedbcd1fdc + languageName: node + linkType: hard + "symbol-tree@npm:^3.2.4": version: 3.2.4 resolution: "symbol-tree@npm:3.2.4" @@ -21080,6 +21426,20 @@ __metadata: languageName: node linkType: hard +"theming@npm:^3.3.0": + version: 3.3.0 + resolution: "theming@npm:3.3.0" + dependencies: + hoist-non-react-statics: "npm:^3.3.0" + prop-types: "npm:^15.5.8" + react-display-name: "npm:^0.2.4" + tiny-warning: "npm:^1.0.2" + peerDependencies: + react: ">=16.3" + checksum: 10c0/15f0eaa3019cb77feb36837d06cb3c1641943e2e3fa06200ae6c996c1b5c7130a3442ddf513cb5723a1b95411287140d39b6bd6fa8ce61abce15eccd7c29c906 + languageName: node + linkType: hard + "thingies@npm:^1.20.0": version: 1.21.0 resolution: "thingies@npm:1.21.0" @@ -21127,6 +21487,13 @@ __metadata: languageName: node linkType: hard +"tiny-warning@npm:^1.0.2": + version: 1.0.3 + resolution: "tiny-warning@npm:1.0.3" + checksum: 10c0/ef8531f581b30342f29670cb41ca248001c6fd7975ce22122bd59b8d62b4fc84ad4207ee7faa95cde982fa3357cd8f4be650142abc22805538c3b1392d7084fa + languageName: node + linkType: hard + "title-case@npm:^3.0.3": version: 3.0.3 resolution: "title-case@npm:3.0.3" From 12e23c20978fc5437c5af1824a27054174ef347d Mon Sep 17 00:00:00 2001 From: Parthiv Krishnan Date: Mon, 23 Jun 2025 14:13:56 -0400 Subject: [PATCH 02/17] fix(bug): added tabContentId to resolve accessibility issue --- package.json | 3 +- .../demos/Animations/examples/Animations.tsx | 10 +- yarn.lock | 251 ++++++++++++++++-- 3 files changed, 237 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index ecae4943afe..206af0b48ad 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "dependencies": { "@patternfly/react-component-groups": "^6.2.1", "clsx": "^2.1.1", - "react-jss": "^10.10.0" + "react-jss": "^10.10.0", + "serve": "^14.2.4" } } diff --git a/packages/react-core/src/demos/Animations/examples/Animations.tsx b/packages/react-core/src/demos/Animations/examples/Animations.tsx index 6f03f327181..769af80c0bc 100644 --- a/packages/react-core/src/demos/Animations/examples/Animations.tsx +++ b/packages/react-core/src/demos/Animations/examples/Animations.tsx @@ -1555,14 +1555,14 @@ export const Animations: FunctionComponent = () => { Resources Everything you need to know about your application setSelectedTab(Number(key))} aria-label="Primary tabs"> - Overview} /> - Resources} /> - Database} /> + Overview} tabContentId="overview" /> + Resources} tabContentId="resources" /> + Database} tabContentId="database" /> {selectedTab === 0 && ( - + {detailStatusEvents} @@ -1570,7 +1570,7 @@ export const Animations: FunctionComponent = () => { )} {selectedTab === 1 && ( - + )} diff --git a/yarn.lock b/yarn.lock index 0c4e5c444e3..fdeb97dd96e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3942,6 +3942,7 @@ __metadata: rollup-plugin-scss: "npm:^4.0.1" rollup-plugin-svg: "npm:^2.0.0" sass: "npm:^1.86.0" + serve: "npm:^14.2.4" surge: "npm:^0.24.6" ts-node: "npm:^10.9.2" ts-patch: "npm:^3.3.0" @@ -5815,6 +5816,13 @@ __metadata: languageName: node linkType: hard +"@zeit/schemas@npm:2.36.0": + version: 2.36.0 + resolution: "@zeit/schemas@npm:2.36.0" + checksum: 10c0/858c3ae46d23122f65d576013dc74f120af0ca7f3256c4b7077bcd12e952c8f71d8241a5165c23d18f6378e198a1db7e93bc8fae8ed0769e4cf4e2df953ee955 + languageName: node + linkType: hard + "@zkochan/js-yaml@npm:0.0.7": version: 0.0.7 resolution: "@zkochan/js-yaml@npm:0.0.7" @@ -6070,6 +6078,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:8.12.0": + version: 8.12.0 + resolution: "ajv@npm:8.12.0" + dependencies: + fast-deep-equal: "npm:^3.1.1" + json-schema-traverse: "npm:^1.0.0" + require-from-string: "npm:^2.0.2" + uri-js: "npm:^4.2.2" + checksum: 10c0/ac4f72adf727ee425e049bc9d8b31d4a57e1c90da8d28bcd23d60781b12fcd6fc3d68db5df16994c57b78b94eed7988f5a6b482fd376dc5b084125e20a0a622e + languageName: node + linkType: hard + "ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" @@ -6113,6 +6133,15 @@ __metadata: languageName: node linkType: hard +"ansi-align@npm:^3.0.1": + version: 3.0.1 + resolution: "ansi-align@npm:3.0.1" + dependencies: + string-width: "npm:^4.1.0" + checksum: 10c0/ad8b755a253a1bc8234eb341e0cec68a857ab18bf97ba2bda529e86f6e30460416523e0ec58c32e5c21f0ca470d779503244892873a5895dbd0c39c788e82467 + languageName: node + linkType: hard + "ansi-colors@npm:^4.1.1": version: 4.1.3 resolution: "ansi-colors@npm:4.1.3" @@ -6245,6 +6274,13 @@ __metadata: languageName: node linkType: hard +"arg@npm:5.0.2": + version: 5.0.2 + resolution: "arg@npm:5.0.2" + checksum: 10c0/ccaf86f4e05d342af6666c569f844bec426595c567d32a8289715087825c2ca7edd8a3d204e4d2fb2aa4602e09a57d0c13ea8c9eea75aac3dbb4af5514e6800e + languageName: node + linkType: hard + "arg@npm:^4.1.0": version: 4.1.3 resolution: "arg@npm:4.1.3" @@ -6987,6 +7023,22 @@ __metadata: languageName: node linkType: hard +"boxen@npm:7.0.0": + version: 7.0.0 + resolution: "boxen@npm:7.0.0" + dependencies: + ansi-align: "npm:^3.0.1" + camelcase: "npm:^7.0.0" + chalk: "npm:^5.0.1" + cli-boxes: "npm:^3.0.0" + string-width: "npm:^5.1.2" + type-fest: "npm:^2.13.0" + widest-line: "npm:^4.0.1" + wrap-ansi: "npm:^8.0.1" + checksum: 10c0/af5e8bc3f1486ac50ec7485ae482eb1d4db905233d7ab2acafc406b576375be85bdc60b53fab99c842c42c274328b7219c7ae79adab13161f4c84e139f4b06ae + languageName: node + linkType: hard + "boxen@npm:^1.2.1": version: 1.3.0 resolution: "boxen@npm:1.3.0" @@ -7322,6 +7374,13 @@ __metadata: languageName: node linkType: hard +"camelcase@npm:^7.0.0": + version: 7.0.1 + resolution: "camelcase@npm:7.0.1" + checksum: 10c0/3adfc9a0e96d51b3a2f4efe90a84dad3e206aaa81dfc664f1bd568270e1bf3b010aad31f01db16345b4ffe1910e16ab411c7273a19a859addd1b98ef7cf4cfbd + languageName: node + linkType: hard + "caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001587": version: 1.0.30001629 resolution: "caniuse-lite@npm:1.0.30001629" @@ -7375,6 +7434,15 @@ __metadata: languageName: node linkType: hard +"chalk-template@npm:0.4.0": + version: 0.4.0 + resolution: "chalk-template@npm:0.4.0" + dependencies: + chalk: "npm:^4.1.2" + checksum: 10c0/6a4cb4252966475f0bd3ee1cd8780146e1ba69f445e59c565cab891ac18708c8143515d23e2b0fb7e192574fb7608d429ea5b28f3b7b9507770ad6fccd3467e3 + languageName: node + linkType: hard + "chalk@npm:4.1.0": version: 4.1.0 resolution: "chalk@npm:4.1.0" @@ -7385,6 +7453,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:5.0.1": + version: 5.0.1 + resolution: "chalk@npm:5.0.1" + checksum: 10c0/97898611ae40cfdeda9778901731df1404ea49fac0eb8253804e8d21b8064917df9823e29c0c9d766aab623da1a0b43d0e072d19a73d4f62d0d9115aef4c64e6 + languageName: node + linkType: hard + "chalk@npm:^2.0.1, chalk@npm:^2.1.0, chalk@npm:^2.4.1, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -7416,6 +7491,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^5.0.1, chalk@npm:^5.4.1": + version: 5.4.1 + resolution: "chalk@npm:5.4.1" + checksum: 10c0/b23e88132c702f4855ca6d25cb5538b1114343e41472d5263ee8a37cccfccd9c4216d111e1097c6a27830407a1dc81fecdf2a56f2c63033d4dbbd88c10b0dcef + languageName: node + linkType: hard + "chalk@npm:^5.3.0": version: 5.3.0 resolution: "chalk@npm:5.3.0" @@ -7423,13 +7505,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.4.1": - version: 5.4.1 - resolution: "chalk@npm:5.4.1" - checksum: 10c0/b23e88132c702f4855ca6d25cb5538b1114343e41472d5263ee8a37cccfccd9c4216d111e1097c6a27830407a1dc81fecdf2a56f2c63033d4dbbd88c10b0dcef - languageName: node - linkType: hard - "change-case@npm:^4.1.2": version: 4.1.2 resolution: "change-case@npm:4.1.2" @@ -7663,6 +7738,13 @@ __metadata: languageName: node linkType: hard +"cli-boxes@npm:^3.0.0": + version: 3.0.0 + resolution: "cli-boxes@npm:3.0.0" + checksum: 10c0/4db3e8fbfaf1aac4fb3a6cbe5a2d3fa048bee741a45371b906439b9ffc821c6e626b0f108bdcd3ddf126a4a319409aedcf39a0730573ff050fdd7b6731e99fb9 + languageName: node + linkType: hard + "cli-cursor@npm:3.1.0, cli-cursor@npm:^3.1.0": version: 3.1.0 resolution: "cli-cursor@npm:3.1.0" @@ -7787,6 +7869,17 @@ __metadata: languageName: node linkType: hard +"clipboardy@npm:3.0.0": + version: 3.0.0 + resolution: "clipboardy@npm:3.0.0" + dependencies: + arch: "npm:^2.2.0" + execa: "npm:^5.1.1" + is-wsl: "npm:^2.2.0" + checksum: 10c0/299d66e13fcaccf656306e76d629ce6927eaba8ba58ae5328e3379ae627e469e29df8ef87408cdb234e2ad0e25f0024dd203393f7e59c67ae79772579c4de052 + languageName: node + linkType: hard + "cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -8129,7 +8222,7 @@ __metadata: languageName: node linkType: hard -"compression@npm:^1.7.4": +"compression@npm:1.7.4, compression@npm:^1.7.4": version: 1.7.4 resolution: "compression@npm:1.7.4" dependencies: @@ -8232,6 +8325,13 @@ __metadata: languageName: node linkType: hard +"content-disposition@npm:0.5.2": + version: 0.5.2 + resolution: "content-disposition@npm:0.5.2" + checksum: 10c0/49eebaa0da1f9609b192e99d7fec31d1178cb57baa9d01f5b63b29787ac31e9d18b5a1033e854c68c9b6cce790e700a6f7fa60e43f95e2e416404e114a8f2f49 + languageName: node + linkType: hard + "content-disposition@npm:0.5.4": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" @@ -10560,7 +10660,7 @@ __metadata: languageName: node linkType: hard -"execa@npm:^5.0.0": +"execa@npm:^5.0.0, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" dependencies: @@ -13536,6 +13636,13 @@ __metadata: languageName: node linkType: hard +"is-port-reachable@npm:4.0.0": + version: 4.0.0 + resolution: "is-port-reachable@npm:4.0.0" + checksum: 10c0/f0fddd9b5c082f7c32356faab38c3c6eab5ea5b54491184f5688f3189d482017d2142c648927ee5964299e4a62da83d41ee52a1d73bf1f700325c370c9ed0cef + languageName: node + linkType: hard + "is-potential-custom-element-name@npm:^1.0.1": version: 1.0.1 resolution: "is-potential-custom-element-name@npm:1.0.1" @@ -15918,6 +16025,22 @@ __metadata: languageName: node linkType: hard +"mime-db@npm:~1.33.0": + version: 1.33.0 + resolution: "mime-db@npm:1.33.0" + checksum: 10c0/79172ce5468c8503b49dddfdddc18d3f5fe2599f9b5fe1bc321a8cbee14c96730fc6db22f907b23701b05b2936f865795f62ec3a78a7f3c8cb2450bb68c6763e + languageName: node + linkType: hard + +"mime-types@npm:2.1.18": + version: 2.1.18 + resolution: "mime-types@npm:2.1.18" + dependencies: + mime-db: "npm:~1.33.0" + checksum: 10c0/a96a8d12f4bb98bc7bfac6a8ccbd045f40368fc1030d9366050c3613825d3715d1c1f393e10a75a885d2cdc1a26cd6d5e11f3a2a0d5c4d361f00242139430a0f + languageName: node + linkType: hard + "mime-types@npm:^2.1.12, mime-types@npm:^2.1.26, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" @@ -16014,6 +16137,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:3.1.2, minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 + languageName: node + linkType: hard + "minimatch@npm:7.4.6, minimatch@npm:^7.3.0": version: 7.4.6 resolution: "minimatch@npm:7.4.6" @@ -16041,15 +16173,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" - dependencies: - brace-expansion: "npm:^1.1.7" - checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 - languageName: node - linkType: hard - "minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": version: 5.1.6 resolution: "minimatch@npm:5.1.6" @@ -17775,7 +17898,7 @@ __metadata: languageName: node linkType: hard -"path-is-inside@npm:^1.0.1, path-is-inside@npm:^1.0.2": +"path-is-inside@npm:1.0.2, path-is-inside@npm:^1.0.1, path-is-inside@npm:^1.0.2": version: 1.0.2 resolution: "path-is-inside@npm:1.0.2" checksum: 10c0/7fdd4b41672c70461cce734fc222b33e7b447fa489c7c4377c95e7e6852d83d69741f307d88ec0cc3b385b41cb4accc6efac3c7c511cd18512e95424f5fa980c @@ -17853,6 +17976,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:3.3.0": + version: 3.3.0 + resolution: "path-to-regexp@npm:3.3.0" + checksum: 10c0/ffa0ebe7088d38d435a8d08b0fe6e8c93ceb2a81a65d4dd1d9a538f52e09d5e3474ed5f553cb3b180d894b0caa10698a68737ab599fd1e56b4663d1a64c9f77b + languageName: node + linkType: hard + "path-type@npm:^3.0.0": version: 3.0.0 resolution: "path-type@npm:3.0.0" @@ -18596,6 +18726,13 @@ __metadata: languageName: node linkType: hard +"range-parser@npm:1.2.0": + version: 1.2.0 + resolution: "range-parser@npm:1.2.0" + checksum: 10c0/c7aef4f6588eb974c475649c157f197d07437d8c6c8ff7e36280a141463fb5ab7a45918417334ebd7b665c6b8321cf31c763f7631dd5f5db9372249261b8b02a + languageName: node + linkType: hard + "range-parser@npm:^1.2.1, range-parser@npm:~1.2.1": version: 1.2.1 resolution: "range-parser@npm:1.2.1" @@ -19011,6 +19148,16 @@ __metadata: languageName: node linkType: hard +"registry-auth-token@npm:3.3.2": + version: 3.3.2 + resolution: "registry-auth-token@npm:3.3.2" + dependencies: + rc: "npm:^1.1.6" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/934b5d504ec6d94d78672dc5e74646c52793e74a6e400c1cffc78838bbb12c5f45e3ef3edba506f3295db794d4dda76f924f2948d48fe1f8e83b6500b0ba53c5 + languageName: node + linkType: hard + "registry-auth-token@npm:^3.0.1": version: 3.4.0 resolution: "registry-auth-token@npm:3.4.0" @@ -19021,7 +19168,7 @@ __metadata: languageName: node linkType: hard -"registry-url@npm:^3.0.3": +"registry-url@npm:3.1.0, registry-url@npm:^3.0.3": version: 3.1.0 resolution: "registry-url@npm:3.1.0" dependencies: @@ -20014,6 +20161,21 @@ __metadata: languageName: node linkType: hard +"serve-handler@npm:6.1.6": + version: 6.1.6 + resolution: "serve-handler@npm:6.1.6" + dependencies: + bytes: "npm:3.0.0" + content-disposition: "npm:0.5.2" + mime-types: "npm:2.1.18" + minimatch: "npm:3.1.2" + path-is-inside: "npm:1.0.2" + path-to-regexp: "npm:3.3.0" + range-parser: "npm:1.2.0" + checksum: 10c0/1e1cb6bbc51ee32bc1505f2e0605bdc2e96605c522277c977b67f83be9d66bd1eec8604388714a4d728e036d86b629bc9aec02120ea030d3d2c3899d44696503 + languageName: node + linkType: hard + "serve-index@npm:^1.9.1": version: 1.9.1 resolution: "serve-index@npm:1.9.1" @@ -20041,6 +20203,27 @@ __metadata: languageName: node linkType: hard +"serve@npm:^14.2.4": + version: 14.2.4 + resolution: "serve@npm:14.2.4" + dependencies: + "@zeit/schemas": "npm:2.36.0" + ajv: "npm:8.12.0" + arg: "npm:5.0.2" + boxen: "npm:7.0.0" + chalk: "npm:5.0.1" + chalk-template: "npm:0.4.0" + clipboardy: "npm:3.0.0" + compression: "npm:1.7.4" + is-port-reachable: "npm:4.0.0" + serve-handler: "npm:6.1.6" + update-check: "npm:1.5.4" + bin: + serve: build/main.js + checksum: 10c0/93abecd6214228d529065040f7c0cbe541c1cc321c6a94b8a968f45a519bd9c46a9fd5e45a9b24a1f5736c5b547b8fa60d5414ebc78f870e29431b64165c1d06 + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -21895,6 +22078,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^2.13.0": + version: 2.19.0 + resolution: "type-fest@npm:2.19.0" + checksum: 10c0/a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb + languageName: node + linkType: hard + "type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -22450,6 +22640,16 @@ __metadata: languageName: node linkType: hard +"update-check@npm:1.5.4": + version: 1.5.4 + resolution: "update-check@npm:1.5.4" + dependencies: + registry-auth-token: "npm:3.3.2" + registry-url: "npm:3.1.0" + checksum: 10c0/ac4b8dafa5db9b1c8ff5d0cfcc3b4c5687c390526b3218155e27173c7ca647572ea9e523dd3463523e698ef94d273768b395748da54655fe773dada59ac9c7b0 + languageName: node + linkType: hard + "update-notifier@npm:^2.2.0": version: 2.5.0 resolution: "update-notifier@npm:2.5.0" @@ -23644,6 +23844,15 @@ __metadata: languageName: node linkType: hard +"widest-line@npm:^4.0.1": + version: 4.0.1 + resolution: "widest-line@npm:4.0.1" + dependencies: + string-width: "npm:^5.0.1" + checksum: 10c0/7da9525ba45eaf3e4ed1a20f3dcb9b85bd9443962450694dae950f4bdd752839747bbc14713522b0b93080007de8e8af677a61a8c2114aa553ad52bde72d0f9c + languageName: node + linkType: hard + "wildcard@npm:^2.0.0": version: 2.0.1 resolution: "wildcard@npm:2.0.1" @@ -23687,7 +23896,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^8.1.0": +"wrap-ansi@npm:^8.0.1, wrap-ansi@npm:^8.1.0": version: 8.1.0 resolution: "wrap-ansi@npm:8.1.0" dependencies: From ef8c9773476a7d938e8dc9e74249f1193ec984d4 Mon Sep 17 00:00:00 2001 From: Parthiv Krishnan Date: Mon, 23 Jun 2025 14:53:57 -0400 Subject: [PATCH 03/17] fix(edit): removed unneeded dependencies and edited verbage in markdown --- package.json | 3 +- .../src/demos/Animations/Animations.md | 15 +- yarn.lock | 251 ++---------------- 3 files changed, 36 insertions(+), 233 deletions(-) diff --git a/package.json b/package.json index 206af0b48ad..ecae4943afe 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,6 @@ "dependencies": { "@patternfly/react-component-groups": "^6.2.1", "clsx": "^2.1.1", - "react-jss": "^10.10.0", - "serve": "^14.2.4" + "react-jss": "^10.10.0" } } diff --git a/packages/react-core/src/demos/Animations/Animations.md b/packages/react-core/src/demos/Animations/Animations.md index 16826a29c58..6da919063ab 100644 --- a/packages/react-core/src/demos/Animations/Animations.md +++ b/packages/react-core/src/demos/Animations/Animations.md @@ -29,7 +29,20 @@ import t_global_text_color_subtle from '@patternfly/react-tokens/dist/esm/t_glob ## Demos -This demonstration highlights PatternFly's latest animations. Explore how components like alerts, navigation, and forms use motion to provide clear feedback and improve usability across the platform. +The following demo highlights the current state of [our ongoing effort to animate PatternFly components](https://github.com/orgs/patternfly/projects/7/views/66). + +To see how components like alerts, navigation, and forms can now use motion to provide clear feedback and improve usability, you can explore this demo and interact with various UI elements. We will continue to update this demo as additional animation support is added. + +Currently, this demo includes animations for: + +* Alerts. +* The notification badge and notification drawer. +* The hamburger/navigation menu icon. +* The masthead settings icon. +* Expandable navigation items. +* Skeleton loader in a table. +* Button clicks. +* Validation failure in forms. ### Animations diff --git a/yarn.lock b/yarn.lock index fdeb97dd96e..0c4e5c444e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3942,7 +3942,6 @@ __metadata: rollup-plugin-scss: "npm:^4.0.1" rollup-plugin-svg: "npm:^2.0.0" sass: "npm:^1.86.0" - serve: "npm:^14.2.4" surge: "npm:^0.24.6" ts-node: "npm:^10.9.2" ts-patch: "npm:^3.3.0" @@ -5816,13 +5815,6 @@ __metadata: languageName: node linkType: hard -"@zeit/schemas@npm:2.36.0": - version: 2.36.0 - resolution: "@zeit/schemas@npm:2.36.0" - checksum: 10c0/858c3ae46d23122f65d576013dc74f120af0ca7f3256c4b7077bcd12e952c8f71d8241a5165c23d18f6378e198a1db7e93bc8fae8ed0769e4cf4e2df953ee955 - languageName: node - linkType: hard - "@zkochan/js-yaml@npm:0.0.7": version: 0.0.7 resolution: "@zkochan/js-yaml@npm:0.0.7" @@ -6078,18 +6070,6 @@ __metadata: languageName: node linkType: hard -"ajv@npm:8.12.0": - version: 8.12.0 - resolution: "ajv@npm:8.12.0" - dependencies: - fast-deep-equal: "npm:^3.1.1" - json-schema-traverse: "npm:^1.0.0" - require-from-string: "npm:^2.0.2" - uri-js: "npm:^4.2.2" - checksum: 10c0/ac4f72adf727ee425e049bc9d8b31d4a57e1c90da8d28bcd23d60781b12fcd6fc3d68db5df16994c57b78b94eed7988f5a6b482fd376dc5b084125e20a0a622e - languageName: node - linkType: hard - "ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" @@ -6133,15 +6113,6 @@ __metadata: languageName: node linkType: hard -"ansi-align@npm:^3.0.1": - version: 3.0.1 - resolution: "ansi-align@npm:3.0.1" - dependencies: - string-width: "npm:^4.1.0" - checksum: 10c0/ad8b755a253a1bc8234eb341e0cec68a857ab18bf97ba2bda529e86f6e30460416523e0ec58c32e5c21f0ca470d779503244892873a5895dbd0c39c788e82467 - languageName: node - linkType: hard - "ansi-colors@npm:^4.1.1": version: 4.1.3 resolution: "ansi-colors@npm:4.1.3" @@ -6274,13 +6245,6 @@ __metadata: languageName: node linkType: hard -"arg@npm:5.0.2": - version: 5.0.2 - resolution: "arg@npm:5.0.2" - checksum: 10c0/ccaf86f4e05d342af6666c569f844bec426595c567d32a8289715087825c2ca7edd8a3d204e4d2fb2aa4602e09a57d0c13ea8c9eea75aac3dbb4af5514e6800e - languageName: node - linkType: hard - "arg@npm:^4.1.0": version: 4.1.3 resolution: "arg@npm:4.1.3" @@ -7023,22 +6987,6 @@ __metadata: languageName: node linkType: hard -"boxen@npm:7.0.0": - version: 7.0.0 - resolution: "boxen@npm:7.0.0" - dependencies: - ansi-align: "npm:^3.0.1" - camelcase: "npm:^7.0.0" - chalk: "npm:^5.0.1" - cli-boxes: "npm:^3.0.0" - string-width: "npm:^5.1.2" - type-fest: "npm:^2.13.0" - widest-line: "npm:^4.0.1" - wrap-ansi: "npm:^8.0.1" - checksum: 10c0/af5e8bc3f1486ac50ec7485ae482eb1d4db905233d7ab2acafc406b576375be85bdc60b53fab99c842c42c274328b7219c7ae79adab13161f4c84e139f4b06ae - languageName: node - linkType: hard - "boxen@npm:^1.2.1": version: 1.3.0 resolution: "boxen@npm:1.3.0" @@ -7374,13 +7322,6 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^7.0.0": - version: 7.0.1 - resolution: "camelcase@npm:7.0.1" - checksum: 10c0/3adfc9a0e96d51b3a2f4efe90a84dad3e206aaa81dfc664f1bd568270e1bf3b010aad31f01db16345b4ffe1910e16ab411c7273a19a859addd1b98ef7cf4cfbd - languageName: node - linkType: hard - "caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001587": version: 1.0.30001629 resolution: "caniuse-lite@npm:1.0.30001629" @@ -7434,15 +7375,6 @@ __metadata: languageName: node linkType: hard -"chalk-template@npm:0.4.0": - version: 0.4.0 - resolution: "chalk-template@npm:0.4.0" - dependencies: - chalk: "npm:^4.1.2" - checksum: 10c0/6a4cb4252966475f0bd3ee1cd8780146e1ba69f445e59c565cab891ac18708c8143515d23e2b0fb7e192574fb7608d429ea5b28f3b7b9507770ad6fccd3467e3 - languageName: node - linkType: hard - "chalk@npm:4.1.0": version: 4.1.0 resolution: "chalk@npm:4.1.0" @@ -7453,13 +7385,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:5.0.1": - version: 5.0.1 - resolution: "chalk@npm:5.0.1" - checksum: 10c0/97898611ae40cfdeda9778901731df1404ea49fac0eb8253804e8d21b8064917df9823e29c0c9d766aab623da1a0b43d0e072d19a73d4f62d0d9115aef4c64e6 - languageName: node - linkType: hard - "chalk@npm:^2.0.1, chalk@npm:^2.1.0, chalk@npm:^2.4.1, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -7491,13 +7416,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.0.1, chalk@npm:^5.4.1": - version: 5.4.1 - resolution: "chalk@npm:5.4.1" - checksum: 10c0/b23e88132c702f4855ca6d25cb5538b1114343e41472d5263ee8a37cccfccd9c4216d111e1097c6a27830407a1dc81fecdf2a56f2c63033d4dbbd88c10b0dcef - languageName: node - linkType: hard - "chalk@npm:^5.3.0": version: 5.3.0 resolution: "chalk@npm:5.3.0" @@ -7505,6 +7423,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^5.4.1": + version: 5.4.1 + resolution: "chalk@npm:5.4.1" + checksum: 10c0/b23e88132c702f4855ca6d25cb5538b1114343e41472d5263ee8a37cccfccd9c4216d111e1097c6a27830407a1dc81fecdf2a56f2c63033d4dbbd88c10b0dcef + languageName: node + linkType: hard + "change-case@npm:^4.1.2": version: 4.1.2 resolution: "change-case@npm:4.1.2" @@ -7738,13 +7663,6 @@ __metadata: languageName: node linkType: hard -"cli-boxes@npm:^3.0.0": - version: 3.0.0 - resolution: "cli-boxes@npm:3.0.0" - checksum: 10c0/4db3e8fbfaf1aac4fb3a6cbe5a2d3fa048bee741a45371b906439b9ffc821c6e626b0f108bdcd3ddf126a4a319409aedcf39a0730573ff050fdd7b6731e99fb9 - languageName: node - linkType: hard - "cli-cursor@npm:3.1.0, cli-cursor@npm:^3.1.0": version: 3.1.0 resolution: "cli-cursor@npm:3.1.0" @@ -7869,17 +7787,6 @@ __metadata: languageName: node linkType: hard -"clipboardy@npm:3.0.0": - version: 3.0.0 - resolution: "clipboardy@npm:3.0.0" - dependencies: - arch: "npm:^2.2.0" - execa: "npm:^5.1.1" - is-wsl: "npm:^2.2.0" - checksum: 10c0/299d66e13fcaccf656306e76d629ce6927eaba8ba58ae5328e3379ae627e469e29df8ef87408cdb234e2ad0e25f0024dd203393f7e59c67ae79772579c4de052 - languageName: node - linkType: hard - "cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -8222,7 +8129,7 @@ __metadata: languageName: node linkType: hard -"compression@npm:1.7.4, compression@npm:^1.7.4": +"compression@npm:^1.7.4": version: 1.7.4 resolution: "compression@npm:1.7.4" dependencies: @@ -8325,13 +8232,6 @@ __metadata: languageName: node linkType: hard -"content-disposition@npm:0.5.2": - version: 0.5.2 - resolution: "content-disposition@npm:0.5.2" - checksum: 10c0/49eebaa0da1f9609b192e99d7fec31d1178cb57baa9d01f5b63b29787ac31e9d18b5a1033e854c68c9b6cce790e700a6f7fa60e43f95e2e416404e114a8f2f49 - languageName: node - linkType: hard - "content-disposition@npm:0.5.4": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" @@ -10660,7 +10560,7 @@ __metadata: languageName: node linkType: hard -"execa@npm:^5.0.0, execa@npm:^5.1.1": +"execa@npm:^5.0.0": version: 5.1.1 resolution: "execa@npm:5.1.1" dependencies: @@ -13636,13 +13536,6 @@ __metadata: languageName: node linkType: hard -"is-port-reachable@npm:4.0.0": - version: 4.0.0 - resolution: "is-port-reachable@npm:4.0.0" - checksum: 10c0/f0fddd9b5c082f7c32356faab38c3c6eab5ea5b54491184f5688f3189d482017d2142c648927ee5964299e4a62da83d41ee52a1d73bf1f700325c370c9ed0cef - languageName: node - linkType: hard - "is-potential-custom-element-name@npm:^1.0.1": version: 1.0.1 resolution: "is-potential-custom-element-name@npm:1.0.1" @@ -16025,22 +15918,6 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:~1.33.0": - version: 1.33.0 - resolution: "mime-db@npm:1.33.0" - checksum: 10c0/79172ce5468c8503b49dddfdddc18d3f5fe2599f9b5fe1bc321a8cbee14c96730fc6db22f907b23701b05b2936f865795f62ec3a78a7f3c8cb2450bb68c6763e - languageName: node - linkType: hard - -"mime-types@npm:2.1.18": - version: 2.1.18 - resolution: "mime-types@npm:2.1.18" - dependencies: - mime-db: "npm:~1.33.0" - checksum: 10c0/a96a8d12f4bb98bc7bfac6a8ccbd045f40368fc1030d9366050c3613825d3715d1c1f393e10a75a885d2cdc1a26cd6d5e11f3a2a0d5c4d361f00242139430a0f - languageName: node - linkType: hard - "mime-types@npm:^2.1.12, mime-types@npm:^2.1.26, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" @@ -16137,15 +16014,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:3.1.2, minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" - dependencies: - brace-expansion: "npm:^1.1.7" - checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 - languageName: node - linkType: hard - "minimatch@npm:7.4.6, minimatch@npm:^7.3.0": version: 7.4.6 resolution: "minimatch@npm:7.4.6" @@ -16173,6 +16041,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 + languageName: node + linkType: hard + "minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": version: 5.1.6 resolution: "minimatch@npm:5.1.6" @@ -17898,7 +17775,7 @@ __metadata: languageName: node linkType: hard -"path-is-inside@npm:1.0.2, path-is-inside@npm:^1.0.1, path-is-inside@npm:^1.0.2": +"path-is-inside@npm:^1.0.1, path-is-inside@npm:^1.0.2": version: 1.0.2 resolution: "path-is-inside@npm:1.0.2" checksum: 10c0/7fdd4b41672c70461cce734fc222b33e7b447fa489c7c4377c95e7e6852d83d69741f307d88ec0cc3b385b41cb4accc6efac3c7c511cd18512e95424f5fa980c @@ -17976,13 +17853,6 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:3.3.0": - version: 3.3.0 - resolution: "path-to-regexp@npm:3.3.0" - checksum: 10c0/ffa0ebe7088d38d435a8d08b0fe6e8c93ceb2a81a65d4dd1d9a538f52e09d5e3474ed5f553cb3b180d894b0caa10698a68737ab599fd1e56b4663d1a64c9f77b - languageName: node - linkType: hard - "path-type@npm:^3.0.0": version: 3.0.0 resolution: "path-type@npm:3.0.0" @@ -18726,13 +18596,6 @@ __metadata: languageName: node linkType: hard -"range-parser@npm:1.2.0": - version: 1.2.0 - resolution: "range-parser@npm:1.2.0" - checksum: 10c0/c7aef4f6588eb974c475649c157f197d07437d8c6c8ff7e36280a141463fb5ab7a45918417334ebd7b665c6b8321cf31c763f7631dd5f5db9372249261b8b02a - languageName: node - linkType: hard - "range-parser@npm:^1.2.1, range-parser@npm:~1.2.1": version: 1.2.1 resolution: "range-parser@npm:1.2.1" @@ -19148,16 +19011,6 @@ __metadata: languageName: node linkType: hard -"registry-auth-token@npm:3.3.2": - version: 3.3.2 - resolution: "registry-auth-token@npm:3.3.2" - dependencies: - rc: "npm:^1.1.6" - safe-buffer: "npm:^5.0.1" - checksum: 10c0/934b5d504ec6d94d78672dc5e74646c52793e74a6e400c1cffc78838bbb12c5f45e3ef3edba506f3295db794d4dda76f924f2948d48fe1f8e83b6500b0ba53c5 - languageName: node - linkType: hard - "registry-auth-token@npm:^3.0.1": version: 3.4.0 resolution: "registry-auth-token@npm:3.4.0" @@ -19168,7 +19021,7 @@ __metadata: languageName: node linkType: hard -"registry-url@npm:3.1.0, registry-url@npm:^3.0.3": +"registry-url@npm:^3.0.3": version: 3.1.0 resolution: "registry-url@npm:3.1.0" dependencies: @@ -20161,21 +20014,6 @@ __metadata: languageName: node linkType: hard -"serve-handler@npm:6.1.6": - version: 6.1.6 - resolution: "serve-handler@npm:6.1.6" - dependencies: - bytes: "npm:3.0.0" - content-disposition: "npm:0.5.2" - mime-types: "npm:2.1.18" - minimatch: "npm:3.1.2" - path-is-inside: "npm:1.0.2" - path-to-regexp: "npm:3.3.0" - range-parser: "npm:1.2.0" - checksum: 10c0/1e1cb6bbc51ee32bc1505f2e0605bdc2e96605c522277c977b67f83be9d66bd1eec8604388714a4d728e036d86b629bc9aec02120ea030d3d2c3899d44696503 - languageName: node - linkType: hard - "serve-index@npm:^1.9.1": version: 1.9.1 resolution: "serve-index@npm:1.9.1" @@ -20203,27 +20041,6 @@ __metadata: languageName: node linkType: hard -"serve@npm:^14.2.4": - version: 14.2.4 - resolution: "serve@npm:14.2.4" - dependencies: - "@zeit/schemas": "npm:2.36.0" - ajv: "npm:8.12.0" - arg: "npm:5.0.2" - boxen: "npm:7.0.0" - chalk: "npm:5.0.1" - chalk-template: "npm:0.4.0" - clipboardy: "npm:3.0.0" - compression: "npm:1.7.4" - is-port-reachable: "npm:4.0.0" - serve-handler: "npm:6.1.6" - update-check: "npm:1.5.4" - bin: - serve: build/main.js - checksum: 10c0/93abecd6214228d529065040f7c0cbe541c1cc321c6a94b8a968f45a519bd9c46a9fd5e45a9b24a1f5736c5b547b8fa60d5414ebc78f870e29431b64165c1d06 - languageName: node - linkType: hard - "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -22078,13 +21895,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^2.13.0": - version: 2.19.0 - resolution: "type-fest@npm:2.19.0" - checksum: 10c0/a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb - languageName: node - linkType: hard - "type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -22640,16 +22450,6 @@ __metadata: languageName: node linkType: hard -"update-check@npm:1.5.4": - version: 1.5.4 - resolution: "update-check@npm:1.5.4" - dependencies: - registry-auth-token: "npm:3.3.2" - registry-url: "npm:3.1.0" - checksum: 10c0/ac4b8dafa5db9b1c8ff5d0cfcc3b4c5687c390526b3218155e27173c7ca647572ea9e523dd3463523e698ef94d273768b395748da54655fe773dada59ac9c7b0 - languageName: node - linkType: hard - "update-notifier@npm:^2.2.0": version: 2.5.0 resolution: "update-notifier@npm:2.5.0" @@ -23844,15 +23644,6 @@ __metadata: languageName: node linkType: hard -"widest-line@npm:^4.0.1": - version: 4.0.1 - resolution: "widest-line@npm:4.0.1" - dependencies: - string-width: "npm:^5.0.1" - checksum: 10c0/7da9525ba45eaf3e4ed1a20f3dcb9b85bd9443962450694dae950f4bdd752839747bbc14713522b0b93080007de8e8af677a61a8c2114aa553ad52bde72d0f9c - languageName: node - linkType: hard - "wildcard@npm:^2.0.0": version: 2.0.1 resolution: "wildcard@npm:2.0.1" @@ -23896,7 +23687,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^8.0.1, wrap-ansi@npm:^8.1.0": +"wrap-ansi@npm:^8.1.0": version: 8.1.0 resolution: "wrap-ansi@npm:8.1.0" dependencies: From 4047a4b6f624fdc1379a8ee06990844b135422e6 Mon Sep 17 00:00:00 2001 From: Parthiv Krishnan Date: Mon, 23 Jun 2025 16:59:12 -0400 Subject: [PATCH 04/17] fix: removed inline styles by using Grid component and added hasAnimations prop to Table --- .../demos/Animations/examples/Animations.tsx | 98 ++++++++++--------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/packages/react-core/src/demos/Animations/examples/Animations.tsx b/packages/react-core/src/demos/Animations/examples/Animations.tsx index 769af80c0bc..39cd1a88a2a 100644 --- a/packages/react-core/src/demos/Animations/examples/Animations.tsx +++ b/packages/react-core/src/demos/Animations/examples/Animations.tsx @@ -44,8 +44,6 @@ import { FormHelperText, FormAlert, FormGroupLabelHelp, - Gallery, - GalleryItem, HelperText, HelperTextItem, Icon, @@ -1149,54 +1147,58 @@ export const Animations: FunctionComponent = () => { ); }; + const DetailsCard: FunctionComponent = () => ( + + + + Details + + + + + + Cluster API Address + + https://api1.devcluster.openshift.com + + + + Cluster ID + 63b97ac1-b850-41d9-8820-239becde9e86 + + + Provide + AWS + + + OpenShift Version + 4.5.0.ci-2020-06-16-015028 + + + Update Channel + stable-4.5 + + + + + + View Settings + + + ); + const detailStatusEvents = ( - - - - - - Details - - - - - - Cluster API Address - - https://api1.devcluster.openshift.com - - - - Cluster ID - 63b97ac1-b850-41d9-8820-239becde9e86 - - - Provide - AWS - - - OpenShift Version - 4.5.0.ci-2020-06-16-015028 - - - Update Channel - stable-4.5 - - - - - - View Settings - - - - + + + + + - - + + - - + + ); const expandableColumns = ['Applications', 'Server', 'Branch', 'Status']; @@ -1244,7 +1246,7 @@ export const Animations: FunctionComponent = () => { {loading ? ( ) : ( - +
    - + - {app.details && isAppExpanded(app) && ( - - - - )} + + + ))}
    Date: Thu, 26 Jun 2025 15:16:02 -0400 Subject: [PATCH 05/17] fix: changed Grid so GridItems stack at small screen widths --- .../src/demos/Animations/examples/Animations.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-core/src/demos/Animations/examples/Animations.tsx b/packages/react-core/src/demos/Animations/examples/Animations.tsx index 39cd1a88a2a..04ff6f7ed60 100644 --- a/packages/react-core/src/demos/Animations/examples/Animations.tsx +++ b/packages/react-core/src/demos/Animations/examples/Animations.tsx @@ -1188,14 +1188,14 @@ export const Animations: FunctionComponent = () => { ); const detailStatusEvents = ( - - + + - + - + @@ -1346,7 +1346,7 @@ export const Animations: FunctionComponent = () => { const handlePasswordChange = (_event, password: string) => { setPassword(password); setIsPasswordValid( - password.length > 12 && /[0-9]/.test(password) && /[A-Z]/.test(password) ? 'success' : 'error' + password.length >= 12 && /[0-9]/.test(password) && /[A-Z]/.test(password) ? 'success' : 'error' ); }; From e5205c0c272f9c68b97b147d16f4481137af2f39 Mon Sep 17 00:00:00 2001 From: Parthiv Krishnan Date: Mon, 30 Jun 2025 10:24:15 -0400 Subject: [PATCH 06/17] fix: added delayed validation on create database form --- .../demos/Animations/examples/Animations.tsx | 104 ++++++++++++++---- 1 file changed, 83 insertions(+), 21 deletions(-) diff --git a/packages/react-core/src/demos/Animations/examples/Animations.tsx b/packages/react-core/src/demos/Animations/examples/Animations.tsx index 04ff6f7ed60..004e2311916 100644 --- a/packages/react-core/src/demos/Animations/examples/Animations.tsx +++ b/packages/react-core/src/demos/Animations/examples/Animations.tsx @@ -1318,44 +1318,96 @@ export const Animations: FunctionComponent = () => { const CreateDatabaseForm: FunctionComponent = () => { const [name, setName] = useState(''); - const [isNameValid, setIsNameValid] = useState('default'); const [email, setEmail] = useState(''); const [version, setVersion] = useState(''); const [isTimeZoneOpen, setIsTimeZoneOpen] = useState(false); const [selectedTimeZone, setSelectedTimeZone] = useState(''); const [password, setPassword] = useState(''); - const [isPasswordValid, setIsPasswordValid] = useState('default'); const [isSuccess, setIsSuccess] = useState(false); const [actionCompleted, setActionCompleted] = useState(false); const labelHelpRef = useRef(null); - const handleNameChange = (_event, name: string) => { + // Re-introducing the type alias for validation status + type validationStatus = 'success' | 'warning' | 'error' | 'default'; + + // Reverting useState to infer the type as a generic string + const [isNameValid, setIsNameValid] = useState('default'); + const [isPasswordValid, setIsPasswordValid] = useState('default'); + const [isNameValidating, setIsNameValidating] = useState(false); + const [isPasswordValidating, setIsPasswordValidating] = useState(false); + + // useEffect for delayed name validation + useEffect(() => { + if (name === '') { + setIsNameValid('default'); + setIsNameValidating(false); + return; + } + + setIsNameValidating(true); + const timerId = setTimeout(() => { + const isValid = name.length > 0 && /^[a-z0-9-]+$/.test(name); + setIsNameValid(isValid ? 'success' : 'error'); + setIsNameValidating(false); + }, 2000); + + return () => { + clearTimeout(timerId); + setIsNameValidating(false); + }; + }, [name]); + + // useEffect for delayed password validation + useEffect(() => { + if (password === '') { + setIsPasswordValid('default'); + setIsPasswordValidating(false); + return; + } + + setIsPasswordValidating(true); + const timerId = setTimeout(() => { + const isValid = password.length >= 12 && /[0-9]/.test(password) && /[A-Z]/.test(password); + setIsPasswordValid(isValid ? 'success' : 'error'); + setIsPasswordValidating(false); + }, 2000); + + return () => { + clearTimeout(timerId); + setIsPasswordValidating(false); + }; + }, [password]); + + const handleNameChange = (_event: React.FormEvent, name: string) => { setName(name); - setIsNameValid(name.length > 0 && /^[a-z0-9-]+$/.test(name) ? 'success' : 'error'); + setIsNameValid('default'); + setIsNameValidating(false); }; - const handleEmailChange = (_event, email: string) => { + const handleEmailChange = (_event: React.FormEvent, email: string) => { setEmail(email); }; - const handleVersionChange = (_event, version: string) => { + const handleVersionChange = (_event: React.FormEvent, version: string) => { setVersion(version); }; - const handlePasswordChange = (_event, password: string) => { + const handlePasswordChange = (_event: React.FormEvent, password: string) => { setPassword(password); - setIsPasswordValid( - password.length >= 12 && /[0-9]/.test(password) && /[A-Z]/.test(password) ? 'success' : 'error' - ); + setIsPasswordValid('default'); + setIsPasswordValidating(false); }; - const onTimeZoneSelect = (_event, selection) => { - setSelectedTimeZone(selection); + const onTimeZoneSelect = ( + _event: React.MouseEvent | React.ChangeEvent | undefined, + selection: string | number | undefined + ) => { + setSelectedTimeZone(selection as string); setIsTimeZoneOpen(false); }; - const timeZoneToggle = (toggleRef) => ( + const timeZoneToggle = (toggleRef: React.Ref) => ( setIsTimeZoneOpen(!isTimeZoneOpen)} isExpanded={isTimeZoneOpen}> {selectedTimeZone || 'Select time zone'} @@ -1441,15 +1493,23 @@ export const Animations: FunctionComponent = () => { aria-describedby="simple-form-name-01-helper" value={name} onChange={handleNameChange} - validated={isNameValid as 'success' | 'warning' | 'error' | 'default'} + validated={isNameValid as validationStatus} /> : } - variant={isNameValid as 'success' | 'warning' | 'error' | 'default'} + icon={isNameValid === 'error' ? : undefined} + variant={isNameValid as validationStatus} > - Must be unique. Can only contain letters, numbers, and hyphens. + {(() => { + if (isNameValidating) { + return 'Validating...'; + } + if (isNameValid === 'error') { + return 'Must contain only lowercase letters, numbers, and hyphens.'; + } + return 'Must be a unique name.'; + })()} @@ -1506,15 +1566,17 @@ export const Animations: FunctionComponent = () => { placeholder="********" value={password} onChange={handlePasswordChange} - validated={isPasswordValid as 'success' | 'warning' | 'error' | 'default'} + validated={isPasswordValid as validationStatus} /> : } - variant={isPasswordValid as 'success' | 'warning' | 'error' | 'default'} + icon={isPasswordValid === 'error' ? : undefined} + variant={isPasswordValid as validationStatus} > - Password must be at least 12 characters and include one uppercase letter and one number. + {isPasswordValidating + ? 'Validating...' + : 'Password must be at least 12 characters and include one uppercase letter and one number.'} From a21727e172a9e39068577ea3b235ddd8f2596427 Mon Sep 17 00:00:00 2001 From: Parthiv Krishnan Date: Tue, 1 Jul 2025 10:22:02 -0400 Subject: [PATCH 07/17] fix: added delayed validation to TimeZone and fixed race condition --- .../src/demos/Animations/Animations.md | 1 + .../demos/Animations/examples/Animations.tsx | 115 ++++++++---------- 2 files changed, 53 insertions(+), 63 deletions(-) diff --git a/packages/react-core/src/demos/Animations/Animations.md b/packages/react-core/src/demos/Animations/Animations.md index 6da919063ab..927c46f4eff 100644 --- a/packages/react-core/src/demos/Animations/Animations.md +++ b/packages/react-core/src/demos/Animations/Animations.md @@ -36,6 +36,7 @@ To see how components like alerts, navigation, and forms can now use motion to p Currently, this demo includes animations for: * Alerts. +* Tabs. * The notification badge and notification drawer. * The hamburger/navigation menu icon. * The masthead settings icon. diff --git a/packages/react-core/src/demos/Animations/examples/Animations.tsx b/packages/react-core/src/demos/Animations/examples/Animations.tsx index 004e2311916..94cc2c458b8 100644 --- a/packages/react-core/src/demos/Animations/examples/Animations.tsx +++ b/packages/react-core/src/demos/Animations/examples/Animations.tsx @@ -1,15 +1,4 @@ -import { - Fragment, - useRef, - useState, - useEffect, - ReactNode, - FunctionComponent, - FormEvent, - RefObject, - MouseEvent, - TransitionEvent -} from 'react'; +import { Fragment, useRef, useState, useEffect, ReactNode, FunctionComponent, FormEvent, RefObject } from 'react'; import { AlertGroup, Alert, @@ -44,6 +33,8 @@ import { FormHelperText, FormAlert, FormGroupLabelHelp, + FormSelect, + FormSelectOption, HelperText, HelperTextItem, Icon, @@ -1320,7 +1311,6 @@ export const Animations: FunctionComponent = () => { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [version, setVersion] = useState(''); - const [isTimeZoneOpen, setIsTimeZoneOpen] = useState(false); const [selectedTimeZone, setSelectedTimeZone] = useState(''); const [password, setPassword] = useState(''); const [isSuccess, setIsSuccess] = useState(false); @@ -1337,6 +1327,9 @@ export const Animations: FunctionComponent = () => { const [isNameValidating, setIsNameValidating] = useState(false); const [isPasswordValidating, setIsPasswordValidating] = useState(false); + const [timeZoneValidated, setTimeZoneValidated] = useState('default'); + const [timeZoneHelperText, setTimeZoneHelperText] = useState('A time zone is required for scheduling tasks.'); + // useEffect for delayed name validation useEffect(() => { if (name === '') { @@ -1379,10 +1372,24 @@ export const Animations: FunctionComponent = () => { }; }, [password]); + useEffect(() => { + if (selectedTimeZone === '') { + setTimeZoneValidated('default'); + setTimeZoneHelperText('A time zone is required for scheduling tasks.'); + return; + } + + const timerId = setTimeout(() => { + setTimeZoneValidated('success'); + setTimeZoneHelperText('Time zone successfully selected.'); + }, 2000); + + return () => clearTimeout(timerId); + }, [selectedTimeZone]); + const handleNameChange = (_event: React.FormEvent, name: string) => { setName(name); setIsNameValid('default'); - setIsNameValidating(false); }; const handleEmailChange = (_event: React.FormEvent, email: string) => { @@ -1396,31 +1403,21 @@ export const Animations: FunctionComponent = () => { const handlePasswordChange = (_event: React.FormEvent, password: string) => { setPassword(password); setIsPasswordValid('default'); - setIsPasswordValidating(false); }; - const onTimeZoneSelect = ( - _event: React.MouseEvent | React.ChangeEvent | undefined, - selection: string | number | undefined - ) => { - setSelectedTimeZone(selection as string); - setIsTimeZoneOpen(false); + const handleTimeZoneChange = (event: React.FormEvent, value: string) => { + setSelectedTimeZone(value); + setTimeZoneValidated('default'); + setTimeZoneHelperText('Validating...'); }; - const timeZoneToggle = (toggleRef: React.Ref) => ( - setIsTimeZoneOpen(!isTimeZoneOpen)} isExpanded={isTimeZoneOpen}> - {selectedTimeZone || 'Select time zone'} - - ); - const handleSubmit = () => { setActionCompleted(true); if ( isPasswordValid === 'success' && isNameValid === 'success' && - selectedTimeZone.length > 0 && - email.includes('@') && - version.length > 0 + timeZoneValidated === 'success' && + email.includes('@') ) { setIsSuccess(true); setTimeout(() => { @@ -1441,7 +1438,7 @@ export const Animations: FunctionComponent = () => { {actionCompleted && (isSuccess ? ( - + { ) : ( - + { /> - : undefined} - variant={isNameValid as validationStatus} - > + {(() => { if (isNameValidating) { return 'Validating...'; } if (isNameValid === 'error') { return 'Must contain only lowercase letters, numbers, and hyphens.'; + } else { + return 'Must be a unique name.'; } - return 'Must be a unique name.'; })()} @@ -1523,39 +1518,38 @@ export const Animations: FunctionComponent = () => { value={email} onChange={handleEmailChange} /> + + Must be a valid email address containing an @ symbol. + - + + + + + + + + {timeZoneHelperText} + + { /> - : undefined} - variant={isPasswordValid as validationStatus} - > + {isPasswordValidating ? 'Validating...' : 'Password must be at least 12 characters and include one uppercase letter and one number.'} @@ -1600,9 +1591,7 @@ export const Animations: FunctionComponent = () => { sidebar={Sidebar} isManagedSidebar notificationDrawer={notificationDrawer} - onNotificationDrawerExpand={( - event: MouseEvent | KeyboardEvent | TransitionEvent - ) => focusDrawer(event)} + onNotificationDrawerExpand={(event) => focusDrawer(event)} isNotificationDrawerExpanded={isDrawerExpanded} skipToContent={PageSkipToContent} // breadcrumb={PageBreadcrumb} From d51f02bc177983967c4fbd6c9f0813ad553cdc01 Mon Sep 17 00:00:00 2001 From: Parthiv Krishnan Date: Mon, 7 Jul 2025 11:56:36 -0400 Subject: [PATCH 08/17] fix: incorporated all feedback from team --- .../demos/Animations/examples/Animations.tsx | 468 ++++++++++-------- 1 file changed, 265 insertions(+), 203 deletions(-) diff --git a/packages/react-core/src/demos/Animations/examples/Animations.tsx b/packages/react-core/src/demos/Animations/examples/Animations.tsx index 94cc2c458b8..60f364a036d 100644 --- a/packages/react-core/src/demos/Animations/examples/Animations.tsx +++ b/packages/react-core/src/demos/Animations/examples/Animations.tsx @@ -39,6 +39,8 @@ import { HelperTextItem, Icon, Label, + List, + ListItem, MenuToggle, Masthead, MastheadMain, @@ -58,7 +60,6 @@ import { NotificationDrawerListItem, NotificationDrawerListItemBody, NotificationDrawerListItemHeader, - NotificationDrawerGroup, Page, PageSection, PageSidebar, @@ -90,14 +91,12 @@ import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon'; -import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon'; import PowerOffIcon from '@patternfly/react-icons/dist/esm/icons/power-off-icon'; import PortIcon from '@patternfly/react-icons/dist/esm/icons/port-icon'; import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon'; import AutomationIcon from '@patternfly/react-icons/dist/esm/icons/automation-icon'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; -import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon'; import imgAvatar from '@patternfly/react-core/src/components/assets/avatarImg.svg'; import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import pfLogo from '@patternfly/react-core/src/demos/assets/pf-logo.PF-HorizontalLogo-Color.svg'; @@ -112,7 +111,12 @@ export const Animations: FunctionComponent = () => { const [isKebabDropdownOpen, setIsKebabDropdownOpen] = useState(false); const [isDrawerExpanded, setIsDrawerExpanded] = useState(false); const [isAlertVisible, setIsAlertVisible] = useState(false); + const [isAlert2Visible, setIsAlert2Visible] = useState(false); + const [isAlert3Visible, setIsAlert3Visible] = useState(false); const [showForm, setShowForm] = useState(false); + const [alert1Id, setAlert1Id] = useState(''); + const [alert2Id, setAlert2Id] = useState(''); + const [alert3Id, setAlert3Id] = useState(''); interface UnreadMap { [notificationId: string]: boolean; @@ -174,6 +178,7 @@ export const Animations: FunctionComponent = () => { setIsAlertVisible(true); setIsUnreadMap((prevUnreadMap) => { const newNotificationId = `notification-${Object.keys(prevUnreadMap || {}).length + 1}`; + setAlert1Id(newNotificationId); return { ...prevUnreadMap, @@ -187,6 +192,44 @@ export const Animations: FunctionComponent = () => { }; }, []); + useEffect(() => { + const timerId = setTimeout(() => { + setIsAlert2Visible(true); + setIsUnreadMap((prevUnreadMap) => { + const newNotificationId = `notification-${Object.keys(prevUnreadMap || {}).length + 1}`; + setAlert2Id(newNotificationId); + + return { + ...prevUnreadMap, + [newNotificationId]: true + }; + }); + }, 14000); + + return () => { + clearTimeout(timerId); + }; + }, []); + + useEffect(() => { + const timerId = setTimeout(() => { + setIsAlert3Visible(true); + setIsUnreadMap((prevUnreadMap) => { + const newNotificationId = `notification-${Object.keys(prevUnreadMap || {}).length + 1}`; + setAlert3Id(newNotificationId); + + return { + ...prevUnreadMap, + [newNotificationId]: true + }; + }); + }, 25000); + + return () => { + clearTimeout(timerId); + }; + }, []); + useEffect(() => { const currentUnread = getNumberUnread(); if (currentUnread > prevUnreadCountRef.current) { @@ -577,11 +620,88 @@ export const Animations: FunctionComponent = () => { {shouldShowNotifications && ( + {/** Alert 1*/} {isAlertVisible && ( onListItemClick(`notification-${Object.keys(isUnreadMap || {}).length}`)} - isRead={isUnreadMap === null || !isUnreadMap[`notification-${Object.keys(isUnreadMap || {}).length}`]} + onClick={() => onListItemClick(alert1Id)} + isRead={isUnreadMap === null || !isUnreadMap[alert1Id]} + > + + !isOpen && closeActionsMenu()} + popperProps={{ position: 'right' }} + toggle={(toggleRef: RefObject) => ( + onToggle(`toggle-${alert1Id}`)} + isExpanded={isActionsMenuOpen[`toggle-${alert1Id}`] || false} + icon={} + /> + )} + > + {notificationDrawerDropdownItems} + + + + A system alert has been triggered. Please review the alert details. + + + )} + {/** Alert 2*/} + {isAlert2Visible && ( + onListItemClick(alert2Id)} + isRead={isUnreadMap === null || !isUnreadMap[alert2Id]} + > + + !isOpen && closeActionsMenu()} + popperProps={{ position: 'right' }} + toggle={(toggleRef: RefObject) => ( + onToggle(`toggle-${alert2Id}`)} + isExpanded={isActionsMenuOpen[`toggle-${alert2Id}`] || false} + icon={} + /> + )} + > + {notificationDrawerDropdownItems} + + + + A system alert has been triggered. Please review the alert details. + + + )} + {/** Alert 3*/} + {isAlert3Visible && ( + onListItemClick(alert3Id)} + isRead={isUnreadMap === null || !isUnreadMap[alert3Id]} > { > !isOpen && closeActionsMenu()} popperProps={{ position: 'right' }} toggle={(toggleRef: RefObject) => ( onToggle(`toggle-id-${Object.keys(isUnreadMap || {}).length}`)} - isExpanded={isActionsMenuOpen[`toggle-id-${Object.keys(isUnreadMap || {}).length}`] || false} + onClick={() => onToggle(`toggle-${alert3Id}`)} + isExpanded={isActionsMenuOpen[`toggle-${alert3Id}`] || false} icon={} /> )} @@ -900,11 +1020,6 @@ export const Animations: FunctionComponent = () => { }; const CardStatus: FunctionComponent = () => { - const [drawerExpanded, setDrawerExpanded] = useState(false); - const handleDrawerToggleClick = () => { - setDrawerExpanded(!drawerExpanded); - }; - const [rowsExpanded, setRowsExpanded] = useState([false, false, false]); const handleToggleExpand = (_: any, rowIndex: number) => { const newRowsExpanded = [...rowsExpanded]; @@ -1005,7 +1120,7 @@ export const Animations: FunctionComponent = () => { const body = ( - + @@ -1072,68 +1187,10 @@ export const Animations: FunctionComponent = () => { ); - const drawerTitle = ( - - - Notifications - - - - - - - - ); - - const drawer = ( - - - - - - - - This is a long description to show how the title will wrap if it is long and wraps to multiple lines. - - - - - - This is a warning notification description. - - - - - - - ); - return ( {header} {body} - - {drawer} ); }; @@ -1233,7 +1290,7 @@ export const Animations: FunctionComponent = () => { }; return ( - + {loading ? ( ) : ( @@ -1256,7 +1313,7 @@ export const Animations: FunctionComponent = () => { {applicationsData.map((app, idx) => (
    { {app.status !== 'Running' && app.status !== 'Degraded' && app.status !== 'Stopped' && app.status}
    - - {app.details} -
    + + {app.details} +
    )} - + ); }; @@ -1308,11 +1363,13 @@ export const Animations: FunctionComponent = () => { ); const CreateDatabaseForm: FunctionComponent = () => { + // State variables const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [version, setVersion] = useState(''); const [selectedTimeZone, setSelectedTimeZone] = useState(''); const [password, setPassword] = useState(''); + // Submit state variables const [isSuccess, setIsSuccess] = useState(false); const [actionCompleted, setActionCompleted] = useState(false); @@ -1324,72 +1381,12 @@ export const Animations: FunctionComponent = () => { // Reverting useState to infer the type as a generic string const [isNameValid, setIsNameValid] = useState('default'); const [isPasswordValid, setIsPasswordValid] = useState('default'); - const [isNameValidating, setIsNameValidating] = useState(false); - const [isPasswordValidating, setIsPasswordValidating] = useState(false); - - const [timeZoneValidated, setTimeZoneValidated] = useState('default'); - const [timeZoneHelperText, setTimeZoneHelperText] = useState('A time zone is required for scheduling tasks.'); - - // useEffect for delayed name validation - useEffect(() => { - if (name === '') { - setIsNameValid('default'); - setIsNameValidating(false); - return; - } - - setIsNameValidating(true); - const timerId = setTimeout(() => { - const isValid = name.length > 0 && /^[a-z0-9-]+$/.test(name); - setIsNameValid(isValid ? 'success' : 'error'); - setIsNameValidating(false); - }, 2000); - - return () => { - clearTimeout(timerId); - setIsNameValidating(false); - }; - }, [name]); - - // useEffect for delayed password validation - useEffect(() => { - if (password === '') { - setIsPasswordValid('default'); - setIsPasswordValidating(false); - return; - } - - setIsPasswordValidating(true); - const timerId = setTimeout(() => { - const isValid = password.length >= 12 && /[0-9]/.test(password) && /[A-Z]/.test(password); - setIsPasswordValid(isValid ? 'success' : 'error'); - setIsPasswordValidating(false); - }, 2000); - - return () => { - clearTimeout(timerId); - setIsPasswordValidating(false); - }; - }, [password]); - - useEffect(() => { - if (selectedTimeZone === '') { - setTimeZoneValidated('default'); - setTimeZoneHelperText('A time zone is required for scheduling tasks.'); - return; - } - - const timerId = setTimeout(() => { - setTimeZoneValidated('success'); - setTimeZoneHelperText('Time zone successfully selected.'); - }, 2000); - - return () => clearTimeout(timerId); - }, [selectedTimeZone]); + const [isEmailValid, setIsEmailValid] = useState('default'); + const [isTimeZoneValid, setIsTimeZoneValid] = useState('default'); + const [errorMessages, setErrorMessages] = useState([]); const handleNameChange = (_event: React.FormEvent, name: string) => { setName(name); - setIsNameValid('default'); }; const handleEmailChange = (_event: React.FormEvent, email: string) => { @@ -1402,34 +1399,74 @@ export const Animations: FunctionComponent = () => { const handlePasswordChange = (_event: React.FormEvent, password: string) => { setPassword(password); - setIsPasswordValid('default'); }; const handleTimeZoneChange = (event: React.FormEvent, value: string) => { setSelectedTimeZone(value); - setTimeZoneValidated('default'); - setTimeZoneHelperText('Validating...'); + }; + + const validateName = (value: string) => /^[a-z0-9-]+$/.test(value) && value.length > 0; + const validatePassword = (value: string) => value.length >= 12 && /[0-9]/.test(value) && /[A-Z]/.test(value); + const validateEmail = (value: string) => value.includes('@'); + const validateTimeZone = (value: string) => value !== ''; + + const handleNameBlur = () => { + setIsNameValid(validateName(name) ? 'success' : 'error'); + }; + + const handlePasswordBlur = () => { + setIsPasswordValid(validatePassword(password) ? 'success' : 'error'); + }; + + const handleEmailBlur = () => { + setIsEmailValid(validateEmail(email) ? 'success' : 'error'); + }; + + const handleTimeZoneBlur = () => { + setIsTimeZoneValid(validateTimeZone(selectedTimeZone) ? 'success' : 'error'); }; const handleSubmit = () => { + const isNameCurrentValid = validateName(name); + const isPasswordCurrentValid = validatePassword(password); + const isEmailCurrentValid = validateEmail(email); + const isTimeZoneCurrentValid = validateTimeZone(selectedTimeZone); + + setIsNameValid(isNameCurrentValid ? 'success' : 'error'); + setIsPasswordValid(isPasswordCurrentValid ? 'success' : 'error'); + setIsEmailValid(isEmailCurrentValid ? 'success' : 'error'); + setIsTimeZoneValid(isTimeZoneCurrentValid ? 'success' : 'error'); + + const allFieldsValid = + isNameCurrentValid && isPasswordCurrentValid && isEmailCurrentValid && isTimeZoneCurrentValid; + setActionCompleted(true); - if ( - isPasswordValid === 'success' && - isNameValid === 'success' && - timeZoneValidated === 'success' && - email.includes('@') - ) { - setIsSuccess(true); + setIsSuccess(allFieldsValid); + if (allFieldsValid) { + setErrorMessages([]); setTimeout(() => { setActionCompleted(false); setIsSuccess(false); - }, 4000); + }, 5000); } else { - setIsSuccess(false); + const errors: string[] = []; + if (!isNameCurrentValid) { + errors.push('Database instance name'); + } + if (!isPasswordCurrentValid) { + errors.push('Admin password'); + } + if (!isEmailCurrentValid) { + errors.push('Admin email'); + } + if (!isTimeZoneCurrentValid) { + errors.push('Time zone'); + } + setErrorMessages(errors); setTimeout(() => { setActionCompleted(false); setIsSuccess(false); - }, 4000); + }, 5000); } }; @@ -1438,31 +1475,37 @@ export const Animations: FunctionComponent = () => { {actionCompleted && (isSuccess ? ( - + ) : ( - + + > + + {errorMessages.map((error) => ( + {error} + ))} + + ))} { aria-describedby="simple-form-name-01-helper" value={name} onChange={handleNameChange} + onBlur={handleNameBlur} validated={isNameValid as validationStatus} /> - - - - {(() => { - if (isNameValidating) { - return 'Validating...'; - } - if (isNameValid === 'error') { - return 'Must contain only lowercase letters, numbers, and hyphens.'; - } else { - return 'Must be a unique name.'; - } - })()} - - - + {isNameValid === 'error' && ( + + + + Must contain only lowercase letters, numbers, and hyphens. + + + + )} { name="simple-form-email-01" value={email} onChange={handleEmailChange} + onBlur={handleEmailBlur} + validated={isEmailValid as validationStatus} /> - - Must be a valid email address containing an @ symbol. - + {isEmailValid === 'error' && ( + + + + Must be a valid email address containing an @ symbol. + + + + )} { value={selectedTimeZone} onChange={handleTimeZoneChange} aria-label="Select time zone" - validated={timeZoneValidated as validationStatus} + onBlur={handleTimeZoneBlur} + validated={isTimeZoneValid as validationStatus} > - - - {timeZoneHelperText} - - + {isTimeZoneValid === 'error' && ( + + + Please select a time zone + + + )} { type="password" id="simple-form-password-01" name="simple-form-password-01" - placeholder="********" value={password} onChange={handlePasswordChange} + onBlur={handlePasswordBlur} validated={isPasswordValid as validationStatus} /> - - - - {isPasswordValidating - ? 'Validating...' - : 'Password must be at least 12 characters and include one uppercase letter and one number.'} - - - + {isPasswordValid === 'error' && ( + + + + Password must be at least 12 characters and include one uppercase letter and one number. + + + + )} + + + + + + + + } + > + {child} + + ); + }; const onToggle = (id: string) => { setIsActionsMenuOpen({ [id]: !isActionsMenuOpen[id] }); }; @@ -258,9 +375,9 @@ export const Animations: FunctionComponent = () => { const markAllRead = () => setIsUnreadMap(null); - const showNotifications = (showNotifications: boolean) => { + const showNotifications = (show: boolean) => { setIsUnreadMap(null); - setShouldShowNotifications(showNotifications); + setShouldShowNotifications(show); }; const focusDrawer = (_event: any) => { @@ -460,15 +577,41 @@ export const Animations: FunctionComponent = () => { }} /** the settings and help icon buttons are only visible on desktop sizes and replaced by a kebab dropdown for other sizes */ > - + . + + + + + + + + ) : null} ); }; From 196c0c3fa2130bd3e4827f47e9db4e1043eaca50 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Tue, 15 Jul 2025 07:02:08 -0400 Subject: [PATCH 10/17] Break up Animation demo into component files --- .../src/demos/Animations/Animations.md | 9 +- .../AnimationsCreateDatabaseForm.tsx | 294 +++ .../Animations/AnimationsHeaderToolbar.tsx | 191 ++ .../AnimationsNotificationsDrawer.tsx | 175 ++ .../demos/Animations/AnimationsOverview.tsx | 206 ++ .../AnimationsOverviewCardStatus.tsx | 82 + .../AnimationsOverviewEventsCard.tsx | 154 ++ .../demos/Animations/AnimationsTourModal.tsx | 46 + .../demos/Animations/GuidedTourContext.tsx | 150 ++ .../demos/Animations/examples/Animations.tsx | 2113 +++-------------- .../react-core/src/demos/Animations/types.ts | 27 + 11 files changed, 1681 insertions(+), 1766 deletions(-) create mode 100644 packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx create mode 100644 packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx create mode 100644 packages/react-core/src/demos/Animations/AnimationsNotificationsDrawer.tsx create mode 100644 packages/react-core/src/demos/Animations/AnimationsOverview.tsx create mode 100644 packages/react-core/src/demos/Animations/AnimationsOverviewCardStatus.tsx create mode 100644 packages/react-core/src/demos/Animations/AnimationsOverviewEventsCard.tsx create mode 100644 packages/react-core/src/demos/Animations/AnimationsTourModal.tsx create mode 100644 packages/react-core/src/demos/Animations/GuidedTourContext.tsx create mode 100644 packages/react-core/src/demos/Animations/types.ts diff --git a/packages/react-core/src/demos/Animations/Animations.md b/packages/react-core/src/demos/Animations/Animations.md index 710d49680be..be9ecefcf1e 100644 --- a/packages/react-core/src/demos/Animations/Animations.md +++ b/packages/react-core/src/demos/Animations/Animations.md @@ -4,7 +4,7 @@ section: design-foundations source: demo --- -import { Fragment, useRef, useState, useEffect } from 'react'; +import { Fragment, useRef, useState, useEffect, useCallback } from 'react'; import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon'; import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon'; @@ -25,7 +25,12 @@ import l_gallery_GridTemplateColumns_min from '@patternfly/react-tokens/dist/esm import {applicationsData} from './examples/ResourceTableData.jsx'; import SkeletonTable from "@patternfly/react-component-groups/dist/dynamic/SkeletonTable"; import t_global_text_color_subtle from '@patternfly/react-tokens/dist/esm/t_global_text_color_subtle'; - +import { AnimationsOverview } from '@patternfly/react-core/dist/js/demos/Animations/AnimationsOverview'; +import { AnimationsNotificationsDrawer } from '@patternfly/react-core/dist/js/demos/Animations/AnimationsNotificationsDrawer'; +import { AnimationsHeaderToolbar } from '@patternfly/react-core/dist/js/demos/Animations/AnimationsHeaderToolbar'; +import { AnimationsTourModal } from '@patternfly/react-core/dist/js/demos/Animations/AnimationsTourModal'; +import { AnimationsCreateDatabaseForm } from '@patternfly/react-core/dist/js/demos/Animations/AnimationsCreateDatabaseForm'; +import { GuidedTourProvider, useGuidedTour } from '@patternfly/react-core/dist/js/demos/Animations/GuidedTourContext'; ## Demos diff --git a/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx b/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx new file mode 100644 index 00000000000..2fd0ee95239 --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx @@ -0,0 +1,294 @@ +import { useRef, useState, FunctionComponent } from 'react'; +import { + AlertGroup, + Alert, + Button, + Form, + FormGroup, + FormHelperText, + FormAlert, + FormGroupLabelHelp, + FormSelect, + FormSelectOption, + HelperText, + HelperTextItem, + List, + ListItem, + TextInput, + Popover, + ActionGroup +} from '../..'; + +interface Props { + onClose: () => void; +} + +export const AnimationsCreateDatabaseForm: FunctionComponent = ({ onClose }) => { + // State variables + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [version, setVersion] = useState(''); + const [selectedTimeZone, setSelectedTimeZone] = useState(''); + const [password, setPassword] = useState(''); + // Submit state variables + const [isSuccess, setIsSuccess] = useState(false); + const [actionCompleted, setActionCompleted] = useState(false); + + const labelHelpRef = useRef(null); + + // Re-introducing the type alias for validation status + type validationStatus = 'success' | 'warning' | 'error' | 'default'; + + // Reverting useState to infer the type as a generic string + const [isNameValid, setIsNameValid] = useState('default'); + const [isPasswordValid, setIsPasswordValid] = useState('default'); + const [isEmailValid, setIsEmailValid] = useState('default'); + const [isTimeZoneValid, setIsTimeZoneValid] = useState('default'); + const [errorMessages, setErrorMessages] = useState([]); + + const handleNameChange = (_event: React.FormEvent, name: string) => { + setName(name); + }; + + const handleEmailChange = (_event: React.FormEvent, email: string) => { + setEmail(email); + }; + + const handleVersionChange = (_event: React.FormEvent, version: string) => { + setVersion(version); + }; + + const handlePasswordChange = (_event: React.FormEvent, password: string) => { + setPassword(password); + }; + + const handleTimeZoneChange = (event: React.FormEvent, value: string) => { + setSelectedTimeZone(value); + }; + + const validateName = (value: string) => /^[a-z0-9-]+$/.test(value) && value.length > 0; + const validatePassword = (value: string) => value.length >= 12 && /[0-9]/.test(value) && /[A-Z]/.test(value); + const validateEmail = (value: string) => value.includes('@'); + const validateTimeZone = (value: string) => value !== ''; + + const handleNameBlur = () => { + setIsNameValid(validateName(name) ? 'success' : 'error'); + }; + + const handlePasswordBlur = () => { + setIsPasswordValid(validatePassword(password) ? 'success' : 'error'); + }; + + const handleEmailBlur = () => { + setIsEmailValid(validateEmail(email) ? 'success' : 'error'); + }; + + const handleTimeZoneBlur = () => { + setIsTimeZoneValid(validateTimeZone(selectedTimeZone) ? 'success' : 'error'); + }; + + const handleSubmit = () => { + const isNameCurrentValid = validateName(name); + const isPasswordCurrentValid = validatePassword(password); + const isEmailCurrentValid = validateEmail(email); + const isTimeZoneCurrentValid = validateTimeZone(selectedTimeZone); + + setIsNameValid(isNameCurrentValid ? 'success' : 'error'); + setIsPasswordValid(isPasswordCurrentValid ? 'success' : 'error'); + setIsEmailValid(isEmailCurrentValid ? 'success' : 'error'); + setIsTimeZoneValid(isTimeZoneCurrentValid ? 'success' : 'error'); + + const allFieldsValid = + isNameCurrentValid && isPasswordCurrentValid && isEmailCurrentValid && isTimeZoneCurrentValid; + + setActionCompleted(true); + setIsSuccess(allFieldsValid); + if (allFieldsValid) { + setErrorMessages([]); + setTimeout(() => { + setActionCompleted(false); + setIsSuccess(false); + }, 5000); + } else { + const errors: string[] = []; + if (!isNameCurrentValid) { + errors.push('Database instance name'); + } + if (!isPasswordCurrentValid) { + errors.push('Admin password'); + } + if (!isEmailCurrentValid) { + errors.push('Admin email'); + } + if (!isTimeZoneCurrentValid) { + errors.push('Time zone'); + } + setErrorMessages(errors); + setTimeout(() => { + setActionCompleted(false); + setIsSuccess(false); + }, 5000); + } + }; + + return ( +
    + {actionCompleted && + (isSuccess ? ( + + + + + + ) : ( + + + + + {errorMessages.map((error) => ( + {error} + ))} + + + + + ))} + The name of your database} + bodyContent={ +
    +

    + The name of your database is used to identify it in the system. It must be unique and cannot be + changed later. +

    +
    + } + > + + + } + isRequired + fieldId="simple-form-name-01" + > + + {isNameValid === 'error' && ( + + + + Must contain only lowercase letters, numbers, and hyphens. + + + + )} +
    + + + {isEmailValid === 'error' && ( + + + + Must be a valid email address containing an @ symbol. + + + + )} + + + + + + + + + + + + {isTimeZoneValid === 'error' && ( + + + Please select a time zone + + + )} + + + + {isPasswordValid === 'error' && ( + + + + Password must be at least 12 characters and include one uppercase letter and one number. + + + + )} + + + + + +
    + ); +}; diff --git a/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx b/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx new file mode 100644 index 00000000000..e6b355011ac --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx @@ -0,0 +1,191 @@ +import { useRef, useState, FunctionComponent, RefObject, useEffect } from 'react'; +import { + Avatar, + Button, + ButtonVariant, + Dropdown, + DropdownItem, + DropdownList, + MenuToggle, + NotificationBadge, + Toolbar, + ToolbarItem, + ToolbarGroup, + ToolbarContent +} from '../..'; +import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon'; +import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; +import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import imgAvatar from '@patternfly/react-core/src/components/assets/avatarImg.svg'; +import { NotificationType } from './types'; +import { useGuidedTour } from './GuidedTourContext'; + +interface Props { + notifications: NotificationType[]; + isDrawerExpanded: boolean; + setIsDrawerExpanded: (newVal: boolean) => void; + onStartGuidedTour: () => void; +} + +export const AnimationsHeaderToolbar: FunctionComponent = ({ + notifications, + isDrawerExpanded, + setIsDrawerExpanded, + onStartGuidedTour +}) => { + const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); + const [isKebabDropdownOpen, setIsKebabDropdownOpen] = useState(false); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [shouldNotifyNewNotification, setShouldNotifyNewNotification] = useState(false); + const { renderTourStepElement, tourStep } = useGuidedTour(); + const previousUnreadCountRef = useRef(notifications.filter((n) => !n.isRead).length); + + const onKebabDropdownSelect = () => setIsKebabDropdownOpen(false); + + const unreadNotificationCount = notifications.filter((n) => !n.isRead).length; + + useEffect(() => { + let timerId: NodeJS.Timeout; + + if (unreadNotificationCount > previousUnreadCountRef.current) { + setShouldNotifyNewNotification(true); + previousUnreadCountRef.current = unreadNotificationCount; + timerId = setTimeout(() => setShouldNotifyNewNotification(false), 1200); + } + return () => { + if (timerId) { + clearTimeout(timerId); + } + }; + }, [unreadNotificationCount]); + + return ( + + + + + + {renderTourStepElement( + 'notificationBadge', + setIsDrawerExpanded(!isDrawerExpanded)} + aria-label="Notifications" + isExpanded={isDrawerExpanded} + count={unreadNotificationCount} + shouldNotify={shouldNotifyNewNotification} + /> + )} + + + + {renderTourStepElement( + 'settingsButton', + + + + + )} + + + ); +}; diff --git a/packages/react-core/src/demos/Animations/AnimationsOverview.tsx b/packages/react-core/src/demos/Animations/AnimationsOverview.tsx new file mode 100644 index 00000000000..6375b46a2bd --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsOverview.tsx @@ -0,0 +1,206 @@ +import { Fragment, FunctionComponent } from 'react'; +import { + Button, + Content, + Card, + CardHeader, + CardBody, + CardFooter, + CardTitle, + ContentVariants, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Divider, + Grid, + GridItem, + Icon, + Label, + PageSection, + Title +} from '../..'; +import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon'; +import PowerOffIcon from '@patternfly/react-icons/dist/esm/icons/power-off-icon'; +import PortIcon from '@patternfly/react-icons/dist/esm/icons/port-icon'; +import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon'; +import AutomationIcon from '@patternfly/react-icons/dist/esm/icons/automation-icon'; +import MultiContentCard from '@patternfly/react-component-groups/dist/dynamic/MultiContentCard'; +import AnimationsOverviewCardStatus from './AnimationsOverviewCardStatus'; +import AnimationsOverviewEventsCard from './AnimationsOverviewEventsCard'; + +export const AnimationsOverview: FunctionComponent = () => { + const cards = [ + // Card 1: Performance + + + + + + + + + Upgrade your kernel version to remediate ntpd time sync issues, kernel panics, network instabilities and + issues with system performance + + + 378 systems + + + + + + + + + + {' '} + System reboot is not required + + + + + + + , + // Card 2: Stability + + + + + + + + + Adjust your networking configuration to get ahead of network performance degradations and packet losses. + + + 211 systems + + + + + + + + + + {' '} + System reboot is required + + + + + + + , + // Card 3: Availability + + + + + + + + + Fine tune your Oracle DB configuration to improve database performance and avoid process failure + + + 166 systems + + + + + + + + + + {' '} + System reboot is not required + + + + + + + + ]; + + return ( + + + + + + + + + + + Details + + + + + + Cluster API Address + + https://api1.devcluster.openshift.com + + + + Cluster ID + 63b97ac1-b850-41d9-8820-239becde9e86 + + + Provide + AWS + + + OpenShift Version + 4.5.0.ci-2020-06-16-015028 + + + Update Channel + stable-4.5 + + + + + + View Settings + + + + + + + + + + + + + ); +}; + +export default AnimationsOverview; diff --git a/packages/react-core/src/demos/Animations/AnimationsOverviewCardStatus.tsx b/packages/react-core/src/demos/Animations/AnimationsOverviewCardStatus.tsx new file mode 100644 index 00000000000..c42d215f91b --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsOverviewCardStatus.tsx @@ -0,0 +1,82 @@ +import { FunctionComponent } from 'react'; +import { Card, CardHeader, CardBody, Flex, FlexItem, Icon, Title, Grid, GridItem } from '../..'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; +import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; + +const AnimationsOverviewCardStatus: FunctionComponent = () => ( + + { + + + Status + + + } + + + + + + + + + + + Cluster + + + + + + + + + + + + e.preventDefault()}> + Control Panel + + + + + + + + + + + + + + Operators + + + 1 degraded + + + + + + + + + + + + + + Image Vulnerabilities + + + 0 vulnerabilities + + + + + + + +); + +export default AnimationsOverviewCardStatus; diff --git a/packages/react-core/src/demos/Animations/AnimationsOverviewEventsCard.tsx b/packages/react-core/src/demos/Animations/AnimationsOverviewEventsCard.tsx new file mode 100644 index 00000000000..901eac6788b --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsOverviewEventsCard.tsx @@ -0,0 +1,154 @@ +import { Fragment, FunctionComponent, useState } from 'react'; +import { + Card, + CardBody, + CardFooter, + CardHeader, + CardTitle, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Divider, + Flex, + FlexItem, + Icon, + MenuToggle, + Select, + SelectList, + SelectOption, + Spinner, + Timestamp, + Title +} from '../..'; +import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; + +const AnimationsOverviewEventsCard: FunctionComponent = () => { + const [isOpen, setIsOpen] = useState(false); + + const selectItems = ( + + + Success + + + Error + + + Warning + + + ); + + const toggle = (toggleRef: any) => ( + setIsOpen(!isOpen)} isExpanded={isOpen} variant="plainText"> + Status + + ); + + const headerActions = ( + + ); + + return ( + + + + + + Events + + + + + + + + + + + + + + + Readiness probe failed + + + + + Readiness probe failed: Get https://10.131.0.7:5000/healthz: dial tcp 10.131.0.7:5000: connect: + connection refused + + + + + + + + + + + + + + + Successful assignment + + + + + Successfully assigned default/example to ip-10-0-130-149.ec2.internal + + + + + + + + + + + + + Pulling image + + + + Pulling image "openshift/hello-openshift" + + + + + + + + + + + + + + Created container + + + + Created container hello-openshift + + + + + + + + + View all events + + + + ); +}; + +export default AnimationsOverviewEventsCard; diff --git a/packages/react-core/src/demos/Animations/AnimationsTourModal.tsx b/packages/react-core/src/demos/Animations/AnimationsTourModal.tsx new file mode 100644 index 00000000000..2bed90f9c4f --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsTourModal.tsx @@ -0,0 +1,46 @@ +import { FunctionComponent } from 'react'; +import { Button, Content, Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant } from '../..'; + +interface Props { + onClose: (startTour?: boolean) => void; +} + +export const AnimationsTourModal: FunctionComponent = ({ onClose }) => ( + onClose()} + aria-labelledby="guided-tour-title" + aria-describedby="guided-tour-description" + > + + + + To see how components like alerts, navigation, and forms can now use motion to provide clear feedback and + improve usability, you can explore this demo and interact with various UI elements. We will continue to update + this demo as additional animation support is added. + + + Get started with a tour to highlight the current state of{' '} + + . + + + + + + + +); diff --git a/packages/react-core/src/demos/Animations/GuidedTourContext.tsx b/packages/react-core/src/demos/Animations/GuidedTourContext.tsx new file mode 100644 index 00000000000..1c6cc08a4d5 --- /dev/null +++ b/packages/react-core/src/demos/Animations/GuidedTourContext.tsx @@ -0,0 +1,150 @@ +import { createContext, useContext, useCallback, useEffect, useState } from 'react'; +import { Button, ButtonVariant, Flex, FlexItem, Popover } from '../..'; +import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; +import { GuidedTourStep } from './types'; + +interface GuidedTourContextType { + onStart: () => void; + onNextStep: () => void; + onPrevStep: () => void; + onFinish: () => void; + tourStep: GuidedTourStep | undefined; + setCustomStepContent: (customContent: React.ReactNode) => void; + renderTourStepElement: (forStepId: string, child: React.ReactElement) => React.ReactElement; +} + +const GuidedTourContext = createContext({ + onStart: () => {}, + onNextStep: () => {}, + onPrevStep: () => {}, + onFinish: () => {}, + setCustomStepContent: () => {}, + renderTourStepElement: () => null, + tourStep: undefined +}); + +export const GuidedTourProvider: React.FC<{ steps: GuidedTourStep[]; children: React.ReactNode }> = ({ + steps, + children +}) => { + const [currentStep, setCurrentStep] = useState(); + const [customStepContent, setCustomStepContent] = useState(); + + useEffect(() => { + setCurrentStep(undefined); + setCustomStepContent(undefined); + }, [steps]); + + const onStart = useCallback(() => { + setCustomStepContent(undefined); + setCurrentStep(0); + }, []); + + const onFinish = useCallback(() => { + setCustomStepContent(undefined); + setCurrentStep(undefined); + }, []); + + const onNextStep = useCallback(() => { + setCustomStepContent(undefined); + setCurrentStep((prev) => { + if (prev === undefined || prev === steps.length) { + return prev; + } + return prev + 1; + }); + }, [steps]); + + const onPrevStep = useCallback(() => { + setCustomStepContent(undefined); + setCurrentStep((prev) => { + if (prev === undefined || prev === 0) { + return prev; + } + return prev - 1; + }); + }, []); + + const tourStep = currentStep !== undefined ? steps[currentStep] : undefined; + + const renderTourStepElement = useCallback( + (forStepId: string, child: React.ReactElement) => { + if (!tourStep || forStepId !== tourStep.stepId) { + return child; + } + return ( + + {tourStep.header} + { + // Had to add a close button here rather than using the showClose property to include the close button + // Using the provided close button requires the 'shouldClose' property to handle the close click, but it also + // gets called on a triggerRef click which we don't want since we ask the user to click the button in order + // to see the animation. I don't see how to distinguish between the close button click and the triggerRef click. + } +
    +
    + + } + bodyContent={customStepContent || tourStep.content} + footerContent={ + + + Step {currentStep + 1}/{steps.length} + + + + + + + + + + + + + } + > + {child} +
    + ); + }, + [tourStep, currentStep, steps, onNextStep, onPrevStep, onFinish, customStepContent] + ); + + return ( + + {children} + + ); +}; +GuidedTourProvider.displayName = 'GuidedTourProvider'; + +export const useGuidedTour = (): GuidedTourContextType => { + const context = useContext(GuidedTourContext); + + if (!context) { + throw new Error('useGuidedTour must be used within a GuidedTourProvider'); + } + return context; +}; diff --git a/packages/react-core/src/demos/Animations/examples/Animations.tsx b/packages/react-core/src/demos/Animations/examples/Animations.tsx index c48298e61ef..f7e44836ecc 100644 --- a/packages/react-core/src/demos/Animations/examples/Animations.tsx +++ b/packages/react-core/src/demos/Animations/examples/Animations.tsx @@ -1,47 +1,18 @@ -import { Fragment, useRef, useState, useEffect, ReactNode, FunctionComponent, FormEvent, RefObject } from 'react'; +import { useRef, useState, useEffect, FunctionComponent, FormEvent, useCallback } from 'react'; import { AlertGroup, Alert, - Avatar, + AlertVariant, Brand, Button, - ButtonVariant, Content, Card, - CardHeader, - CardBody, - CardFooter, - CardTitle, ContentVariants, - Divider, - Dropdown, - DropdownItem, - DropdownList, - DescriptionList, - DescriptionListGroup, - DescriptionListTerm, - DescriptionListDescription, EmptyState, EmptyStateActions, EmptyStateBody, EmptyStateFooter, - EmptyStateVariant, - Flex, - FlexItem, - Form, - FormGroup, - FormHelperText, - FormAlert, - FormGroupLabelHelp, - FormSelect, - FormSelectOption, - HelperText, - HelperTextItem, - Icon, Label, - List, - ListItem, - MenuToggle, Masthead, MastheadMain, MastheadBrand, @@ -52,67 +23,35 @@ import { NavItem, NavList, NavExpandable, - NotificationBadge, - NotificationDrawer, - NotificationDrawerBody, - NotificationDrawerHeader, - NotificationDrawerList, - NotificationDrawerListItem, - NotificationDrawerListItemBody, - NotificationDrawerListItemHeader, Page, PageSection, PageSidebar, PageSidebarBody, PageToggleButton, - Select, - SelectList, - SelectOption, - Spinner, SkipToContent, - Toolbar, - TextInput, - ToolbarItem, - ToolbarGroup, - ToolbarContent, Tabs, Tab, - TabTitleText, - Title, - Timestamp, - Popover, - ActionGroup, - Grid, - GridItem, - Modal, - ModalBody, - ModalFooter, - ModalHeader, - ModalVariant + TabTitleText } from '@patternfly/react-core'; import { Table, Thead, Tbody, Tr, Th, Td, ExpandableRowContent } from '@patternfly/react-table'; -import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon'; -import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; -import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; -import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; -import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon'; -import PowerOffIcon from '@patternfly/react-icons/dist/esm/icons/power-off-icon'; -import PortIcon from '@patternfly/react-icons/dist/esm/icons/port-icon'; -import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon'; -import AutomationIcon from '@patternfly/react-icons/dist/esm/icons/automation-icon'; -import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; -import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; +import SkeletonTable from '@patternfly/react-component-groups/dist/dynamic/SkeletonTable'; import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; -import TimesIcon from '@patternfly/react-icons/icons/times-icon/dist/esm/icons/times-icon'; -// @ts-ignore -import imgAvatar from '@patternfly/react-core/src/components/assets/avatarImg.svg'; // @ts-ignore import pfLogo from '@patternfly/react-core/src/demos/assets/pf-logo.PF-HorizontalLogo-Color.svg'; -import MultiContentCard from '@patternfly/react-component-groups/dist/dynamic/MultiContentCard'; +import { Application, NotificationType } from '../types'; +import { AnimationsOverview } from '../AnimationsOverview'; +import { AnimationsNotificationsDrawer } from '../AnimationsNotificationsDrawer'; +import { AnimationsCreateDatabaseForm } from '../AnimationsCreateDatabaseForm'; +import { GuidedTourProvider, useGuidedTour } from '../GuidedTourContext'; +import { AnimationsHeaderToolbar } from '../AnimationsHeaderToolbar'; +import { AnimationsTourModal } from '../AnimationsTourModal'; import { applicationsData } from './ResourceTableData'; -import SkeletonTable from '@patternfly/react-component-groups/dist/dynamic/SkeletonTable'; -const GuidedTourSteps = [ +const mainContainerPageId = 'main-content-page-layout-default-nav'; +const expandableColumns = ['Applications', 'Server', 'Branch', 'Status']; +const initialExpandedServerNames = ['Cost Management']; // Default to expanded + +export const GuidedTourSteps = [ { stepId: 'settingsButton', header:
    Cog Button
    , @@ -134,99 +73,119 @@ const GuidedTourSteps = [ Collapse and then expand hamburger to see this animation in action. ) + }, + { + stepId: 'notificationBadge', + header:
    Notification badge
    , + content: ( + <> + + Another animation is for the notification badge when a new notification arrives. + + Watch the notification badge to see it ring when the notification is received. + + ) } ]; -export const Animations: FunctionComponent = () => { +const AnimationsPage: FunctionComponent = () => { const drawerRef = useRef(null); - - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [isKebabDropdownOpen, setIsKebabDropdownOpen] = useState(false); const [isDrawerExpanded, setIsDrawerExpanded] = useState(false); - const [isAlertVisible, setIsAlertVisible] = useState(false); - const [isAlert2Visible, setIsAlert2Visible] = useState(false); - const [isAlert3Visible, setIsAlert3Visible] = useState(false); + const [notifications, setNotifications] = useState([ + { + id: 'notification-1', + title: 'Unread info notification title', + message: 'This is an info notification description.', + variant: AlertVariant.info, + timeout: 3000, + timeoutAnimation: 3000, + isNew: false, + isRead: false + }, + { + id: 'notification-2', + title: + 'Unread danger notification title. This is a long title to show how the title will wrap if it is long and wraps to multiple lines.', + message: + 'This is a danger notification description. This is a long description to show how the title will wrap if it is long and wraps to multiple lines.', + variant: AlertVariant.danger, + timeout: 3000, + timeoutAnimation: 3000, + isNew: false, + isRead: true + }, + { + id: 'notification-3', + title: 'Read warning notification title', + message: 'This is a warning notification description.', + variant: AlertVariant.warning, + timeout: 3000, + timeoutAnimation: 3000, + isNew: false, + isRead: true + }, + { + id: 'notification-4', + title: 'Read success notification title', + message: 'This is a success notification description.', + variant: AlertVariant.success, + timeout: 3000, + timeoutAnimation: 3000, + isNew: false, + isRead: true + } + ]); + const [selectedTab, setSelectedTab] = useState(0); const [showForm, setShowForm] = useState(false); - const [alert1Id, setAlert1Id] = useState(''); - const [alert2Id, setAlert2Id] = useState(''); - const [alert3Id, setAlert3Id] = useState(''); const [showTourModal, setShowTourModal] = useState(true); - - const [currentStep, setCurrentStep] = useState(); - - const startNotifications = () => { - setIsUnreadMap((prevUnreadMap) => { - const newNotificationId = `notification-${Object.keys(prevUnreadMap || {}).length + 1}`; - setAlert1Id(newNotificationId); - - return { - ...prevUnreadMap, - [newNotificationId]: true - }; - }); - setIsAlertVisible(true); - }; - - const onStart = () => setCurrentStep(0); - - const onFinish = () => { - setCurrentStep(undefined); - startNotifications(); - }; - - const onNextStep = () => - setCurrentStep((prev) => { - if (prev === undefined || prev === GuidedTourSteps.length) { - return prev; - } - return prev + 1; - }); - - const onPrevStep = () => - setCurrentStep((prev) => { - if (prev === undefined || prev === 0) { - return prev; - } - return prev - 1; - }); - - const tourStep = currentStep !== undefined ? GuidedTourSteps[currentStep] : undefined; - - interface UnreadMap { - [notificationId: string]: boolean; - } - const [activeItem, setActiveItem] = useState(0); const [activeGroup, setActiveGroup] = useState(null); - const [isUnreadMap, setIsUnreadMap] = useState({ - 'notification-1': true, - 'notification-2': true, - 'notification-3': false, - 'notification-4': false - }); + const { onStart, renderTourStepElement, setCustomStepContent, tourStep } = useGuidedTour(); - const [shouldShowNotifications, setShouldShowNotifications] = useState(true); - - interface ActionsMenu { - [toggleId: string]: boolean; - } - - const handleShowForm = () => { - setShowForm(!showForm); - }; - - const [isActionsMenuOpen, setIsActionsMenuOpen] = useState({}); - - const [selectedTab, setSelectedTab] = useState(0); - - const [shouldNotify, setShouldNotify] = useState(false); - const prevUnreadCountRef = useRef(0); + const addNotification = useCallback((showToast = true) => { + setNotifications((prev) => [ + { + id: `new-notification-${prev.length + 1}`, + title: 'Animated notification', + message: 'A system alert has been triggered. Please review the alert details.', + variant: AlertVariant.danger, + timeout: 3000, + timeoutAnimation: 3000, + isNew: showToast, + isRead: false + }, + ...prev + ]); + }, []); - const getNumberUnread = () => { - if (!isUnreadMap) { - return 0; + useEffect(() => { + if (tourStep?.stepId === 'notificationBadge') { + setCustomStepContent( + <> + + Another animation is for the notification badge when a new notification arrives. + + Watch the notification badge to see it ring when a notification is received. + + + + + ); } - return Object.values(isUnreadMap).filter(Boolean).length; + }, [tourStep?.stepId, setCustomStepContent, addNotification]); + + const startNotifications = () => { + setTimeout(() => { + addNotification(); + }, 1000); + setTimeout(() => { + addNotification(); + }, 5000); + setTimeout(() => { + addNotification(); + }, 9000); }; const onNavSelect = ( @@ -241,145 +200,6 @@ export const Animations: FunctionComponent = () => { setActiveGroup(selectedItem.groupId as string | null); }; - const onDropdownToggle = () => setIsDropdownOpen((prevState) => !prevState); - const onDropdownSelect = () => setIsDropdownOpen(false); - const onKebabDropdownToggle = () => setIsKebabDropdownOpen((prevState) => !prevState); - const onKebabDropdownSelect = () => setIsKebabDropdownOpen(false); - const onCloseNotificationDrawer = (_event: any) => setIsDrawerExpanded((prevState) => !prevState); - - useEffect(() => { - let timerId; - if (isAlertVisible) { - timerId = setTimeout(() => { - setIsAlert2Visible(true); - setIsUnreadMap((prevUnreadMap) => { - const newNotificationId = `notification-${Object.keys(prevUnreadMap || {}).length + 1}`; - setAlert2Id(newNotificationId); - - return { - ...prevUnreadMap, - [newNotificationId]: true - }; - }); - }, 10000); - } - - return () => { - if (timerId) { - clearTimeout(timerId); - } - }; - }, [isAlertVisible]); - - useEffect(() => { - let timerId; - if (isAlert2Visible) { - timerId = setTimeout(() => { - setIsAlert3Visible(true); - setIsUnreadMap((prevUnreadMap) => { - const newNotificationId = `notification-${Object.keys(prevUnreadMap || {}).length + 1}`; - setAlert3Id(newNotificationId); - - return { - ...prevUnreadMap, - [newNotificationId]: true - }; - }); - }, 10000); - } - return () => { - if (timerId) { - clearTimeout(timerId); - } - }; - }, [isAlert2Visible]); - - useEffect(() => { - const currentUnread = getNumberUnread(); - if (currentUnread > prevUnreadCountRef.current) { - setShouldNotify(true); - setTimeout(() => setShouldNotify(false), 1000); // Reset after animation - } - prevUnreadCountRef.current = currentUnread; - }, [isUnreadMap, getNumberUnread]); - - useEffect(() => { - prevUnreadCountRef.current = getNumberUnread(); - }, [getNumberUnread]); - - const renderTourStepElement = (forStepId: string, child: React.ReactElement) => { - if (!tourStep || forStepId !== tourStep.stepId) { - return child; - } - return ( - - {tourStep.header} -
    -
    - - } - bodyContent={<>{tourStep.content}} - footerContent={ - - - Step {currentStep + 1}/{GuidedTourSteps.length} - - - - - - - - - - - - - } - > - {child} -
    - ); - }; - const onToggle = (id: string) => { - setIsActionsMenuOpen({ [id]: !isActionsMenuOpen[id] }); - }; - - const closeActionsMenu = () => setIsActionsMenuOpen({}); - - const onListItemClick = (id: string) => { - if (!isUnreadMap) { - return; - } - setIsUnreadMap({ ...isUnreadMap, [id]: false }); - }; - - const markAllRead = () => setIsUnreadMap(null); - - const showNotifications = (show: boolean) => { - setIsUnreadMap(null); - setShouldShowNotifications(show); - }; - const focusDrawer = (_event: any) => { if (drawerRef.current === null) { return; @@ -391,1499 +211,264 @@ export const Animations: FunctionComponent = () => { firstTabbableItem?.focus(); }; - const cards = [ - // Card 1: Performance - - - - - - - - - Upgrade your kernel version to remediate ntpd time sync issues, kernel panics, network instabilities and - issues with system performance - - - 378 systems - - - - - - - - - - {' '} - System reboot is not required - - - - - - - , - // Card 2: Stability - - - - - - - - - Adjust your networking configuration to get ahead of network performance degradations and packet losses. - - - 211 systems - - - - - - - - - - {' '} - System reboot is required - - - - - - - , - // Card 3: Availability - - - - - - - - - Fine tune your Oracle DB configuration to improve database performance and avoid process failure - - - 166 systems - - - - - - - - - - {' '} - System reboot is not required - - - - - - - - ]; - - const PageNav = ( - - ); - const kebabDropdownItems = ( - <> - - Settings - - - Help - - - ); - const userDropdownItems = ( - <> - My profile - User management - Logout - - ); - const headerToolbar = ( - - - - - - onCloseNotificationDrawer(event)} - aria-label="Notifications" - isExpanded={isDrawerExpanded} - count={getNumberUnread()} - shouldNotify={shouldNotify} - /> - - - - {renderTourStepElement( - 'settingsButton', - - - - - )} - - - ); - const EventsCard: FunctionComponent = () => { - const [isOpen, setIsOpen] = useState(false); + {alert.message} + + ))} + + Resources + Everything you need to know about your application + setSelectedTab(Number(key))} aria-label="Primary tabs"> + Overview} tabContentId="overview" /> + Resources} tabContentId="resources" /> + Database} tabContentId="database" /> + + + {selectedTab === 0 && } + + {selectedTab === 1 && ( + + + + )} - const selectItems = ( - - - Success - - - Error - - - Warning - - - ); + {selectedTab === 2 && ( + + {showForm ? ( + setShowForm(false)} /> + ) : ( + + No results match the filter criteria. Clear all filters and try again. + + + + + + + )} + + )} + {showTourModal ? : null} + + ); +}; - const toggle = (toggleRef) => ( - setIsOpen(!isOpen)} isExpanded={isOpen} variant="plainText"> - Status - - ); +// Can't break this into a separate file, seems we need to stay in the examples dir when using '@patternfly/react-table' +const AnimationsResourcesTable: FunctionComponent = () => { + const [areAllExpanded, setAreAllExpanded] = useState(false); + const [collapseAllAriaLabel, setCollapseAllAriaLabel] = useState('Expand all'); + const [expandedAppNames, setExpandedAppNames] = useState(initialExpandedServerNames); + const [loading, setLoading] = useState(true); - const headerActions = ( - - ); + useEffect(() => { + const timer = setTimeout(() => setLoading(false), 2000); + return () => clearTimeout(timer); + }, []); - return ( - - - - - - Events - - - - - - - - - - - - - - - Readiness probe failed - - - - - Readiness probe failed: Get https://10.131.0.7:5000/healthz: dial tcp 10.131.0.7:5000: connect: - connection refused - - - - - - - - - - - - - - - Successful assignment - - - - - Successfully assigned default/example to ip-10-0-130-149.ec2.internal - - - - - - - - - - - - - Pulling image - - - - Pulling image "openshift/hello-openshift" - - - - - - - - - - - - - - Created container - - - - Created container hello-openshift - - - - - - - - - View all events - - - - ); + useEffect(() => { + const allExpanded = expandedAppNames.length === applicationsData.length; + setAreAllExpanded(allExpanded); + setCollapseAllAriaLabel(allExpanded ? 'Collapse all' : 'Expand all'); + }, [expandedAppNames]); + + const setAppExpanded = (app: Application, isExpanding: boolean) => { + const others = expandedAppNames.filter((n) => n !== app.name); + setExpandedAppNames(isExpanding ? [...others, app.name] : others); }; - const CardStatus: FunctionComponent = () => { - const [rowsExpanded, setRowsExpanded] = useState([false, false, false]); - const handleToggleExpand = (_: any, rowIndex: number) => { - const newRowsExpanded = [...rowsExpanded]; - newRowsExpanded[rowIndex] = !rowsExpanded[rowIndex]; - setRowsExpanded(newRowsExpanded); - }; - - const header = ( - - - Status - - - ); - - const columns = ['Components', 'Response Rate']; + const isAppExpanded = (app: Application) => expandedAppNames.includes(app.name); - const rows = [ - { - content: ['API Servers', '20%'], - child: ( - - ) - }, - { - content: ['Controller Managers', '100%'], - child: ( - - ) - }, - { - content: ['etcd', '91%'], - child: ( - - ) - } - ]; + const onCollapseAll = (_event: any, _rowIndex: number, isOpen: boolean) => { + setExpandedAppNames(isOpen ? applicationsData.map((app) => app.name) : []); + }; - const popoverBodyContent = ( - <> -
    - Components of the Control Panel are responsible for maintaining and reconciling the state of the cluster. -
    - + return ( + + {loading ? ( + + ) : ( +
    - + ))} - {rows.map((row, rowIndex) => { - const parentRow = ( - + + {applicationsData.map((app, idx) => ( + + - ))} + + + + - ); - const childRow = row.child ? ( - - + - ) : null; - return ( - - {parentRow} - {childRow} - - ); - })} + + ))}
    - {columns.map((column, columnIndex) => ( - - {column} - + {expandableColumns.map((column) => ( + {column}
    setAppExpanded(app, !isAppExpanded(app)) + } + : undefined + } /> - {row.content.map((cell, cellIndex) => ( - - {cell} - {app.name}{app.header}{app.branch} + {app.status === 'Running' && } + {app.status === 'Degraded' && } + {app.status === 'Stopped' && } + {app.status !== 'Running' && app.status !== 'Degraded' && app.status !== 'Stopped' && app.status} +
    - {row.child} +
    + + {app.details}
    - - ); - - const body = ( - - - - - - - - - - - Cluster - - - - - - - - - - - - - e.preventDefault()}> - Control Panel - - - - - - - - - - - - - - - Operators - - - 1 degraded - - - - - - - - - - - - - - Image Vulnerabilities - - - 0 vulnerabilities - - - - - - - ); - - return ( - - {header} - {body} - - ); - }; - - const DetailsCard: FunctionComponent = () => ( - - - - Details - - - - - - Cluster API Address - - https://api1.devcluster.openshift.com - - - - Cluster ID - 63b97ac1-b850-41d9-8820-239becde9e86 - - - Provide - AWS - - - OpenShift Version - 4.5.0.ci-2020-06-16-015028 - - - Update Channel - stable-4.5 - - - - - - View Settings - + )} ); - - const detailStatusEvents = ( - - - - - - - - - - - - ); - - const expandableColumns = ['Applications', 'Server', 'Branch', 'Status']; - - interface Application { - name: string; - header: string; - branch: string; - status: string; - details?: ReactNode; - } - - const initialExpandedServerNames = ['Cost Management']; // Default to expanded - - const TableExpandCollapseAll: FunctionComponent = () => { - const [areAllExpanded, setAreAllExpanded] = useState(false); - const [collapseAllAriaLabel, setCollapseAllAriaLabel] = useState('Expand all'); - const [expandedAppNames, setExpandedAppNames] = useState(initialExpandedServerNames); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const timer = setTimeout(() => setLoading(false), 2000); - return () => clearTimeout(timer); - }, []); - - useEffect(() => { - const allExpanded = expandedAppNames.length === applicationsData.length; - setAreAllExpanded(allExpanded); - setCollapseAllAriaLabel(allExpanded ? 'Collapse all' : 'Expand all'); - }, [expandedAppNames]); - - const setAppExpanded = (app: Application, isExpanding: boolean) => { - const others = expandedAppNames.filter((n) => n !== app.name); - setExpandedAppNames(isExpanding ? [...others, app.name] : others); - }; - - const isAppExpanded = (app: Application) => expandedAppNames.includes(app.name); - - const onCollapseAll = (_event: any, _rowIndex: number, isOpen: boolean) => { - setExpandedAppNames(isOpen ? applicationsData.map((app) => app.name) : []); - }; - - return ( - - {loading ? ( - - ) : ( - - - - - ))} - - - - {applicationsData.map((app, idx) => ( - - - - - - - - - - - - ))} -
    - {expandableColumns.map((column) => ( - {column}
    setAppExpanded(app, !isAppExpanded(app)) - } - : undefined - } - /> - {app.name}{app.header}{app.branch} - {app.status === 'Running' && } - {app.status === 'Degraded' && } - {app.status === 'Stopped' && } - {app.status !== 'Running' && app.status !== 'Degraded' && app.status !== 'Stopped' && app.status} -
    - - {app.details} -
    - )} -
    - ); - }; - - const EmptyStateNoMatchFound: FunctionComponent = () => ( - - No results match the filter criteria. Clear all filters and try again. - - - - - - - ); - - const CreateDatabaseForm: FunctionComponent = () => { - // State variables - const [name, setName] = useState(''); - const [email, setEmail] = useState(''); - const [version, setVersion] = useState(''); - const [selectedTimeZone, setSelectedTimeZone] = useState(''); - const [password, setPassword] = useState(''); - // Submit state variables - const [isSuccess, setIsSuccess] = useState(false); - const [actionCompleted, setActionCompleted] = useState(false); - - const labelHelpRef = useRef(null); - - // Re-introducing the type alias for validation status - type validationStatus = 'success' | 'warning' | 'error' | 'default'; - - // Reverting useState to infer the type as a generic string - const [isNameValid, setIsNameValid] = useState('default'); - const [isPasswordValid, setIsPasswordValid] = useState('default'); - const [isEmailValid, setIsEmailValid] = useState('default'); - const [isTimeZoneValid, setIsTimeZoneValid] = useState('default'); - const [errorMessages, setErrorMessages] = useState([]); - - const handleNameChange = (_event: React.FormEvent, name: string) => { - setName(name); - }; - - const handleEmailChange = (_event: React.FormEvent, email: string) => { - setEmail(email); - }; - - const handleVersionChange = (_event: React.FormEvent, version: string) => { - setVersion(version); - }; - - const handlePasswordChange = (_event: React.FormEvent, password: string) => { - setPassword(password); - }; - - const handleTimeZoneChange = (event: React.FormEvent, value: string) => { - setSelectedTimeZone(value); - }; - - const validateName = (value: string) => /^[a-z0-9-]+$/.test(value) && value.length > 0; - const validatePassword = (value: string) => value.length >= 12 && /[0-9]/.test(value) && /[A-Z]/.test(value); - const validateEmail = (value: string) => value.includes('@'); - const validateTimeZone = (value: string) => value !== ''; - - const handleNameBlur = () => { - setIsNameValid(validateName(name) ? 'success' : 'error'); - }; - - const handlePasswordBlur = () => { - setIsPasswordValid(validatePassword(password) ? 'success' : 'error'); - }; - - const handleEmailBlur = () => { - setIsEmailValid(validateEmail(email) ? 'success' : 'error'); - }; - - const handleTimeZoneBlur = () => { - setIsTimeZoneValid(validateTimeZone(selectedTimeZone) ? 'success' : 'error'); - }; - - const handleSubmit = () => { - const isNameCurrentValid = validateName(name); - const isPasswordCurrentValid = validatePassword(password); - const isEmailCurrentValid = validateEmail(email); - const isTimeZoneCurrentValid = validateTimeZone(selectedTimeZone); - - setIsNameValid(isNameCurrentValid ? 'success' : 'error'); - setIsPasswordValid(isPasswordCurrentValid ? 'success' : 'error'); - setIsEmailValid(isEmailCurrentValid ? 'success' : 'error'); - setIsTimeZoneValid(isTimeZoneCurrentValid ? 'success' : 'error'); - - const allFieldsValid = - isNameCurrentValid && isPasswordCurrentValid && isEmailCurrentValid && isTimeZoneCurrentValid; - - setActionCompleted(true); - setIsSuccess(allFieldsValid); - if (allFieldsValid) { - setErrorMessages([]); - setTimeout(() => { - setActionCompleted(false); - setIsSuccess(false); - }, 5000); - } else { - const errors: string[] = []; - if (!isNameCurrentValid) { - errors.push('Database instance name'); - } - if (!isPasswordCurrentValid) { - errors.push('Admin password'); - } - if (!isEmailCurrentValid) { - errors.push('Admin email'); - } - if (!isTimeZoneCurrentValid) { - errors.push('Time zone'); - } - setErrorMessages(errors); - setTimeout(() => { - setActionCompleted(false); - setIsSuccess(false); - }, 5000); - } - }; - - return ( -
    - {actionCompleted && - (isSuccess ? ( - - - - - - ) : ( - - - - - {errorMessages.map((error) => ( - {error} - ))} - - - - - ))} - The name of your database} - bodyContent={ -
    -

    - The name of your database is used to identify it in the system. It must be unique and cannot be - changed later. -

    -
    - } - > - - - } - isRequired - fieldId="simple-form-name-01" - > - - {isNameValid === 'error' && ( - - - - Must contain only lowercase letters, numbers, and hyphens. - - - - )} -
    - - - {isEmailValid === 'error' && ( - - - - Must be a valid email address containing an @ symbol. - - - - )} - - - - - - - - - - - - {isTimeZoneValid === 'error' && ( - - - Please select a time zone - - - )} - - - - {isPasswordValid === 'error' && ( - - - - Password must be at least 12 characters and include one uppercase letter and one number. - - - - )} - - - - - -
    - ); - }; - - const closeTourModal = (startTour = false) => { - setShowTourModal(false); - startTour ? onStart() : startNotifications(); - }; - - return ( - - focusDrawer(event)} - isNotificationDrawerExpanded={isDrawerExpanded} - skipToContent={PageSkipToContent} - // breadcrumb={PageBreadcrumb} - mainContainerId={pageId} - > - - {isAlertVisible && ( - - - Something wicked this way comes - - - )} - {isAlert2Visible && ( - - - Something wicked this way comes - - - )} - {isAlert3Visible && ( - - - Something wicked this way comes - - - )} - Resources - Everything you need to know about your application - setSelectedTab(Number(key))} aria-label="Primary tabs"> - Overview} tabContentId="overview" /> - Resources} tabContentId="resources" /> - Database} tabContentId="database" /> - - - {selectedTab === 0 && ( - - - - - {detailStatusEvents} - - )} - - {selectedTab === 1 && ( - - - - )} - - {selectedTab === 2 && ( - {showForm ? : } - )} - - {showTourModal ? ( - closeTourModal()} - aria-labelledby="guided-tour-title" - aria-describedby="guided-tour-description" - > - - - - To see how components like alerts, navigation, and forms can now use motion to provide clear feedback and - improve usability, you can explore this demo and interact with various UI elements. We will continue to - update this demo as additional animation support is added. - - - Get started with a tour to highlight the current state of{' '} - - . - - - - - - - - ) : null} - - ); }; + +export const Animations: FunctionComponent = () => ( + + + +); diff --git a/packages/react-core/src/demos/Animations/types.ts b/packages/react-core/src/demos/Animations/types.ts new file mode 100644 index 00000000000..72a1490d11f --- /dev/null +++ b/packages/react-core/src/demos/Animations/types.ts @@ -0,0 +1,27 @@ +import { AlertVariant } from '../..'; +import { ReactNode } from 'react'; + +export interface NotificationType { + id: string; + title: string; + message: string; + variant: AlertVariant; + timeout: number; + timeoutAnimation: number; + isNew: boolean; + isRead: boolean; +} + +export interface Application { + name: string; + header: string; + branch: string; + status: string; + details?: ReactNode; +} + +export interface GuidedTourStep { + stepId: string; + header: React.ReactNode; + content: React.ReactNode; +} From 336064b5f13a24b210261eca5f5107e61bfcc541 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Tue, 15 Jul 2025 14:37:44 -0400 Subject: [PATCH 11/17] Add additional Guided tour steps --- .../AnimationsCreateDatabaseForm.tsx | 89 +++------ .../Animations/AnimationsHeaderToolbar.tsx | 8 +- .../demos/Animations/AnimationsTourModal.tsx | 30 +-- .../demos/Animations/GuidedTourContext.tsx | 2 + .../demos/Animations/examples/Animations.tsx | 172 +++++++++++++++--- .../react-core/src/demos/Animations/types.ts | 1 + 6 files changed, 189 insertions(+), 113 deletions(-) diff --git a/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx b/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx index 2fd0ee95239..f64a840f2fd 100644 --- a/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx +++ b/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx @@ -12,12 +12,11 @@ import { FormSelectOption, HelperText, HelperTextItem, - List, - ListItem, TextInput, Popover, ActionGroup } from '../..'; +import { useGuidedTour } from './GuidedTourContext'; interface Props { onClose: () => void; @@ -33,6 +32,7 @@ export const AnimationsCreateDatabaseForm: FunctionComponent = ({ onClose // Submit state variables const [isSuccess, setIsSuccess] = useState(false); const [actionCompleted, setActionCompleted] = useState(false); + const { renderTourStepElement } = useGuidedTour(); const labelHelpRef = useRef(null); @@ -44,7 +44,6 @@ export const AnimationsCreateDatabaseForm: FunctionComponent = ({ onClose const [isPasswordValid, setIsPasswordValid] = useState('default'); const [isEmailValid, setIsEmailValid] = useState('default'); const [isTimeZoneValid, setIsTimeZoneValid] = useState('default'); - const [errorMessages, setErrorMessages] = useState([]); const handleNameChange = (_event: React.FormEvent, name: string) => { setName(name); @@ -103,68 +102,31 @@ export const AnimationsCreateDatabaseForm: FunctionComponent = ({ onClose setActionCompleted(true); setIsSuccess(allFieldsValid); - if (allFieldsValid) { - setErrorMessages([]); - setTimeout(() => { - setActionCompleted(false); - setIsSuccess(false); - }, 5000); - } else { - const errors: string[] = []; - if (!isNameCurrentValid) { - errors.push('Database instance name'); - } - if (!isPasswordCurrentValid) { - errors.push('Admin password'); - } - if (!isEmailCurrentValid) { - errors.push('Admin email'); - } - if (!isTimeZoneCurrentValid) { - errors.push('Time zone'); - } - setErrorMessages(errors); - setTimeout(() => { - setActionCompleted(false); - setIsSuccess(false); - }, 5000); - } }; - return ( + const onReset = () => { + setIsNameValid('default'); + setIsPasswordValid('default'); + setIsEmailValid('default'); + setIsTimeZoneValid('default'); + }; + + return renderTourStepElement( + 'validationErrors',
    - {actionCompleted && - (isSuccess ? ( - - - - - - ) : ( - - - - - {errorMessages.map((error) => ( - {error} - ))} - - - - - ))} + {actionCompleted && isSuccess ? ( + + + + + + ) : null} = ({ onClose + ); diff --git a/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx b/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx index e6b355011ac..7bb31ac3a83 100644 --- a/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx +++ b/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx @@ -26,13 +26,15 @@ interface Props { isDrawerExpanded: boolean; setIsDrawerExpanded: (newVal: boolean) => void; onStartGuidedTour: () => void; + onEndGuidedTour: () => void; } export const AnimationsHeaderToolbar: FunctionComponent = ({ notifications, isDrawerExpanded, setIsDrawerExpanded, - onStartGuidedTour + onStartGuidedTour, + onEndGuidedTour }) => { const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); const [isKebabDropdownOpen, setIsKebabDropdownOpen] = useState(false); @@ -116,8 +118,8 @@ export const AnimationsHeaderToolbar: FunctionComponent = ({ )} > - onStartGuidedTour()} isDisabled={!!tourStep}> - Guided tour + (tourStep ? onEndGuidedTour() : onStartGuidedTour())}> + {tourStep ? 'End guided tour' : 'Guided tour'} diff --git a/packages/react-core/src/demos/Animations/AnimationsTourModal.tsx b/packages/react-core/src/demos/Animations/AnimationsTourModal.tsx index 2bed90f9c4f..7dba5d3591d 100644 --- a/packages/react-core/src/demos/Animations/AnimationsTourModal.tsx +++ b/packages/react-core/src/demos/Animations/AnimationsTourModal.tsx @@ -1,5 +1,5 @@ import { FunctionComponent } from 'react'; -import { Button, Content, Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant } from '../..'; +import { Button, Content, Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant } from '../../index'; interface Props { onClose: (startTour?: boolean) => void; @@ -13,33 +13,19 @@ export const AnimationsTourModal: FunctionComponent = ({ onClose }) => ( aria-labelledby="guided-tour-title" aria-describedby="guided-tour-description" > - + - To see how components like alerts, navigation, and forms can now use motion to provide clear feedback and - improve usability, you can explore this demo and interact with various UI elements. We will continue to update - this demo as additional animation support is added. - - - Get started with a tour to highlight the current state of{' '} - - . + Welcome! Many of our components now use motion to engage users, provide clear feedback, and improve usability. + Let's explore some of these new animations and see how they work in a real UI - + diff --git a/packages/react-core/src/demos/Animations/GuidedTourContext.tsx b/packages/react-core/src/demos/Animations/GuidedTourContext.tsx index 1c6cc08a4d5..a77643b43b4 100644 --- a/packages/react-core/src/demos/Animations/GuidedTourContext.tsx +++ b/packages/react-core/src/demos/Animations/GuidedTourContext.tsx @@ -76,7 +76,9 @@ export const GuidedTourProvider: React.FC<{ steps: GuidedTourStep[]; children: R {tourStep.header} diff --git a/packages/react-core/src/demos/Animations/examples/Animations.tsx b/packages/react-core/src/demos/Animations/examples/Animations.tsx index f7e44836ecc..bd1d708a1cd 100644 --- a/packages/react-core/src/demos/Animations/examples/Animations.tsx +++ b/packages/react-core/src/demos/Animations/examples/Animations.tsx @@ -38,7 +38,7 @@ import SkeletonTable from '@patternfly/react-component-groups/dist/dynamic/Skele import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; // @ts-ignore import pfLogo from '@patternfly/react-core/src/demos/assets/pf-logo.PF-HorizontalLogo-Color.svg'; -import { Application, NotificationType } from '../types'; +import { Application, GuidedTourStep, NotificationType } from '../types'; import { AnimationsOverview } from '../AnimationsOverview'; import { AnimationsNotificationsDrawer } from '../AnimationsNotificationsDrawer'; import { AnimationsCreateDatabaseForm } from '../AnimationsCreateDatabaseForm'; @@ -51,40 +51,106 @@ const mainContainerPageId = 'main-content-page-layout-default-nav'; const expandableColumns = ['Applications', 'Server', 'Branch', 'Status']; const initialExpandedServerNames = ['Cost Management']; // Default to expanded -export const GuidedTourSteps = [ +export const GuidedTourSteps: GuidedTourStep[] = [ + { + stepId: 'toastNotifications', + header:
    Alerts
    , + content: '===== This content is customized ======' + }, { stepId: 'settingsButton', - header:
    Cog Button
    , + header:
    Buttons: Settings
    , content: ( <> - Hover over the cog. Notice the settings cog with it's little rotation. - Click on the button and you can also see the new ripple effect on buttons + + Hover over the settings button. The cog icon rotates to show that it’s interactive. + + Click it to see the new ripple effect we've added to all buttons. ) }, { stepId: 'navToggle', - header:
    Masthead hamburger menu
    , + header:
    Buttons: Hamburger menu
    , content: ( <> + Hover over the hamburger menu to see an arrow indicator appear. - One more icon animation is the new hamburger menu with a slick little directional arrow. + Click the button and watch the arrow's direction change as the menu opens and closes, always showing you what + will happen next. - Collapse and then expand hamburger to see this animation in action. ) }, { stepId: 'notificationBadge', - header:
    Notification badge
    , + header:
    Buttons: Notification badge
    , + content: '===== This content is customized ======' + }, + { + stepId: 'tabs', + header:
    Tabs
    , + position: 'top', + content: ( + + Click between the different tabs and watch how the active tab indicator smoothly slides to your selection, + providing clear feedback on your location. + + ) + }, + { + stepId: 'skeletonLoader', + header:
    Skeleton loader
    , + position: 'top', + content: ( + <> + + Watch how the loading indicators animate to inform the user that there is processing going on behind the + scenes. + + + ) + }, + { + stepId: 'expandableComponents', + header:
    Expandable components
    , + position: 'top', + content: ( + <> + Click to expand this hidden content section. + + Notice how the hidden information smoothly fades and slides into place. Click again to collapse it and see the + reverse animation. + + + ) + }, + { + stepId: 'validationErrors', + header:
    Validation errors
    , content: ( <> - Another animation is for the notification badge when a new notification arrives. + Click the Submit button while fields are empty to trigger an error. Watch the input fields + jiggle from side to side, drawing your attention to issues that need fixing. - Watch the notification badge to see it ring when the notification is received. + Reduced-motion users will only see the fade, not the jiggle. ) + // }, + // { + // stepId: 'progressStepper', + // header:
    In process indicator
    , + // content: ( + // <> + // + // Watch as a process starts for step 2. + // + // + // When a task is running, the in-process icon now spins in place, providing clear and continuous feedback that the system is working. + // + // + // ) } ]; @@ -140,7 +206,7 @@ const AnimationsPage: FunctionComponent = () => { const [showTourModal, setShowTourModal] = useState(true); const [activeItem, setActiveItem] = useState(0); const [activeGroup, setActiveGroup] = useState(null); - const { onStart, renderTourStepElement, setCustomStepContent, tourStep } = useGuidedTour(); + const { onStart, onFinish, renderTourStepElement, setCustomStepContent, tourStep } = useGuidedTour(); const addNotification = useCallback((showToast = true) => { setNotifications((prev) => [ @@ -163,9 +229,11 @@ const AnimationsPage: FunctionComponent = () => { setCustomStepContent( <> - Another animation is for the notification badge when a new notification arrives. + Click Add notification. Watch for a new notification to arrive. + + + The bell icon "rings" with a subtle rotation to quickly catch your as a message comes in. - Watch the notification badge to see it ring when a notification is received. + + + ); + } + if (tourStep?.stepId === 'expandableComponents' || tourStep?.stepId === 'skeletonLoader') { + setSelectedTab(1); + } + if (tourStep?.stepId === 'validationErrors') { + setSelectedTab(2); + setShowForm(true); + } }, [tourStep?.stepId, setCustomStepContent, addNotification]); const startNotifications = () => { @@ -239,6 +332,7 @@ const AnimationsPage: FunctionComponent = () => { isDrawerExpanded={isDrawerExpanded} setIsDrawerExpanded={setIsDrawerExpanded} onStartGuidedTour={() => setShowTourModal(true)} + onEndGuidedTour={() => onFinish()} /> @@ -312,12 +406,15 @@ const AnimationsPage: FunctionComponent = () => { mainContainerId={mainContainerPageId} > - - {notifications - .filter((n) => n.isNew) - .map((alert) => ( + {renderTourStepElement( + 'toastNotifications', +
    + )} + {notifications + .filter((n) => n.isNew) + .map((alert) => ( + { > {alert.message} - ))} - + + ))} Resources Everything you need to know about your application setSelectedTab(Number(key))} aria-label="Primary tabs"> Overview} tabContentId="overview" /> - Resources} tabContentId="resources" /> + {renderTourStepElement( + 'tabs', + Resources} tabContentId="resources" /> + )} Database} tabContentId="database" /> @@ -384,11 +484,21 @@ const AnimationsResourcesTable: FunctionComponent = () => { const [collapseAllAriaLabel, setCollapseAllAriaLabel] = useState('Expand all'); const [expandedAppNames, setExpandedAppNames] = useState(initialExpandedServerNames); const [loading, setLoading] = useState(true); + const { tourStep, renderTourStepElement } = useGuidedTour(); useEffect(() => { - const timer = setTimeout(() => setLoading(false), 2000); - return () => clearTimeout(timer); - }, []); + let timer: NodeJS.Timeout; + if (loading && tourStep?.stepId !== 'skeletonLoader') { + timer = setTimeout(() => setLoading(false), 2000); + } else if (!loading && tourStep?.stepId === 'skeletonLoader') { + setLoading(true); + } + return () => { + if (timer) { + clearTimeout(timer); + } + }; + }, [loading, tourStep?.stepId]); useEffect(() => { const allExpanded = expandedAppNames.length === applicationsData.length; @@ -410,10 +520,20 @@ const AnimationsResourcesTable: FunctionComponent = () => { return ( {loading ? ( - + <> + {renderTourStepElement( + 'skeletonLoader', +
    + )} + + ) : ( + {renderTourStepElement( + 'expandableComponents', +
    + )}
    Date: Wed, 16 Jul 2025 08:13:50 -0400 Subject: [PATCH 12/17] animations guided tour: Add ending page and better mobile support Also updated imports locations so hot reloads work properly --- .../src/demos/Animations/Animations.md | 13 +- .../Animations/AnimationsEndTourModal.tsx | 30 +++++ .../Animations/AnimationsHeaderToolbar.tsx | 8 +- ...Modal.tsx => AnimationsStartTourModal.tsx} | 2 +- .../demos/Animations/GuidedTourContext.tsx | 70 +++++++++-- .../demos/Animations/examples/Animations.tsx | 118 ++++++++++++++---- 6 files changed, 189 insertions(+), 52 deletions(-) create mode 100644 packages/react-core/src/demos/Animations/AnimationsEndTourModal.tsx rename packages/react-core/src/demos/Animations/{AnimationsTourModal.tsx => AnimationsStartTourModal.tsx} (92%) diff --git a/packages/react-core/src/demos/Animations/Animations.md b/packages/react-core/src/demos/Animations/Animations.md index be9ecefcf1e..03af1156981 100644 --- a/packages/react-core/src/demos/Animations/Animations.md +++ b/packages/react-core/src/demos/Animations/Animations.md @@ -25,12 +25,13 @@ import l_gallery_GridTemplateColumns_min from '@patternfly/react-tokens/dist/esm import {applicationsData} from './examples/ResourceTableData.jsx'; import SkeletonTable from "@patternfly/react-component-groups/dist/dynamic/SkeletonTable"; import t_global_text_color_subtle from '@patternfly/react-tokens/dist/esm/t_global_text_color_subtle'; -import { AnimationsOverview } from '@patternfly/react-core/dist/js/demos/Animations/AnimationsOverview'; -import { AnimationsNotificationsDrawer } from '@patternfly/react-core/dist/js/demos/Animations/AnimationsNotificationsDrawer'; -import { AnimationsHeaderToolbar } from '@patternfly/react-core/dist/js/demos/Animations/AnimationsHeaderToolbar'; -import { AnimationsTourModal } from '@patternfly/react-core/dist/js/demos/Animations/AnimationsTourModal'; -import { AnimationsCreateDatabaseForm } from '@patternfly/react-core/dist/js/demos/Animations/AnimationsCreateDatabaseForm'; -import { GuidedTourProvider, useGuidedTour } from '@patternfly/react-core/dist/js/demos/Animations/GuidedTourContext'; +import { AnimationsOverview } from '@patternfly/react-core/dist/esm/demos/Animations/AnimationsOverview'; +import { AnimationsNotificationsDrawer } from '@patternfly/react-core/dist/esm/demos/Animations/AnimationsNotificationsDrawer'; +import { AnimationsHeaderToolbar } from '@patternfly/react-core/dist/esm/demos/Animations/AnimationsHeaderToolbar'; +import { AnimationsStartTourModal } from '@patternfly/react-core/dist/esm/demos/Animations/AnimationsStartTourModal'; +import { AnimationsEndTourModal } from '@patternfly/react-core/dist/esm/demos/Animations/AnimationsEndTourModal'; +import { AnimationsCreateDatabaseForm } from '@patternfly/react-core/dist/esm/demos/Animations/AnimationsCreateDatabaseForm'; +import { GuidedTourProvider, useGuidedTour } from '@patternfly/react-core/dist/esm/demos/Animations/GuidedTourContext'; ## Demos diff --git a/packages/react-core/src/demos/Animations/AnimationsEndTourModal.tsx b/packages/react-core/src/demos/Animations/AnimationsEndTourModal.tsx new file mode 100644 index 00000000000..b330297926b --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsEndTourModal.tsx @@ -0,0 +1,30 @@ +import { FunctionComponent } from 'react'; +import { Button, Content, Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant } from '../../index'; +import { useGuidedTour } from './GuidedTourContext'; + +export const AnimationsEndTourModal: FunctionComponent = () => { + const { onStart, onFinish } = useGuidedTour(); + + return ( + + + + Come back again to see the progress we've been making! + + + + + + + ); +}; diff --git a/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx b/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx index 7bb31ac3a83..f01dd0ad8d3 100644 --- a/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx +++ b/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx @@ -80,13 +80,7 @@ export const AnimationsHeaderToolbar: FunctionComponent = ({ /> )} - + {renderTourStepElement( 'settingsButton', diff --git a/packages/react-core/src/demos/Animations/AnimationsTourModal.tsx b/packages/react-core/src/demos/Animations/AnimationsStartTourModal.tsx similarity index 92% rename from packages/react-core/src/demos/Animations/AnimationsTourModal.tsx rename to packages/react-core/src/demos/Animations/AnimationsStartTourModal.tsx index 7dba5d3591d..cf7abe069b0 100644 --- a/packages/react-core/src/demos/Animations/AnimationsTourModal.tsx +++ b/packages/react-core/src/demos/Animations/AnimationsStartTourModal.tsx @@ -5,7 +5,7 @@ interface Props { onClose: (startTour?: boolean) => void; } -export const AnimationsTourModal: FunctionComponent = ({ onClose }) => ( +export const AnimationsStartTourModal: FunctionComponent = ({ onClose }) => ( void; onFinish: () => void; tourStep: GuidedTourStep | undefined; + isFinished: boolean; setCustomStepContent: (customContent: React.ReactNode) => void; renderTourStepElement: (forStepId: string, child: React.ReactElement) => React.ReactElement; } @@ -20,7 +21,8 @@ const GuidedTourContext = createContext({ onFinish: () => {}, setCustomStepContent: () => {}, renderTourStepElement: () => null, - tourStep: undefined + tourStep: undefined, + isFinished: false }); export const GuidedTourProvider: React.FC<{ steps: GuidedTourStep[]; children: React.ReactNode }> = ({ @@ -29,6 +31,10 @@ export const GuidedTourProvider: React.FC<{ steps: GuidedTourStep[]; children: R }) => { const [currentStep, setCurrentStep] = useState(); const [customStepContent, setCustomStepContent] = useState(); + const [windowWidth, setWindowWidth] = useState(); + const unObserver = useRef(null); + + const isMobile = windowWidth < 500; useEffect(() => { setCurrentStep(undefined); @@ -48,12 +54,12 @@ export const GuidedTourProvider: React.FC<{ steps: GuidedTourStep[]; children: R const onNextStep = useCallback(() => { setCustomStepContent(undefined); setCurrentStep((prev) => { - if (prev === undefined || prev === steps.length) { + if (prev === undefined) { return prev; } return prev + 1; }); - }, [steps]); + }, []); const onPrevStep = useCallback(() => { setCustomStepContent(undefined); @@ -66,6 +72,7 @@ export const GuidedTourProvider: React.FC<{ steps: GuidedTourStep[]; children: R }, []); const tourStep = currentStep !== undefined ? steps[currentStep] : undefined; + const isFinished = currentStep !== undefined ? currentStep >= steps.length : false; const renderTourStepElement = useCallback( (forStepId: string, child: React.ReactElement) => { @@ -76,7 +83,7 @@ export const GuidedTourProvider: React.FC<{ steps: GuidedTourStep[]; children: R - @@ -129,13 +133,53 @@ export const GuidedTourProvider: React.FC<{ steps: GuidedTourStep[]; children: R ); }, - [tourStep, currentStep, steps, onNextStep, onPrevStep, onFinish, customStepContent] + [tourStep, currentStep, steps, onNextStep, onPrevStep, onFinish, customStepContent, isMobile] + ); + + const measureRef = (ref: HTMLDivElement) => { + // Remove any previous observer + if (unObserver.current) { + unObserver.current(); + } + + if (!ref) { + return; + } + + const handleResize = () => setWindowWidth(ref.clientWidth); + + // Set size on initialization + handleResize(); + + const debounceResize = debounce(handleResize, 100); + + // Update graph size on resize events + unObserver.current = getResizeObserver(ref, debounceResize); + }; + + useEffect( + () => () => { + if (unObserver.current) { + unObserver.current(); + } + }, + [] ); return ( +
    {children} ); diff --git a/packages/react-core/src/demos/Animations/examples/Animations.tsx b/packages/react-core/src/demos/Animations/examples/Animations.tsx index bd1d708a1cd..eb266e1fc5a 100644 --- a/packages/react-core/src/demos/Animations/examples/Animations.tsx +++ b/packages/react-core/src/demos/Animations/examples/Animations.tsx @@ -8,10 +8,12 @@ import { Content, Card, ContentVariants, + debounce, EmptyState, EmptyStateActions, EmptyStateBody, EmptyStateFooter, + getResizeObserver, Label, Masthead, MastheadMain, @@ -39,12 +41,13 @@ import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; // @ts-ignore import pfLogo from '@patternfly/react-core/src/demos/assets/pf-logo.PF-HorizontalLogo-Color.svg'; import { Application, GuidedTourStep, NotificationType } from '../types'; -import { AnimationsOverview } from '../AnimationsOverview'; -import { AnimationsNotificationsDrawer } from '../AnimationsNotificationsDrawer'; -import { AnimationsCreateDatabaseForm } from '../AnimationsCreateDatabaseForm'; -import { GuidedTourProvider, useGuidedTour } from '../GuidedTourContext'; -import { AnimationsHeaderToolbar } from '../AnimationsHeaderToolbar'; -import { AnimationsTourModal } from '../AnimationsTourModal'; +import { AnimationsOverview } from '../../../../dist/esm/demos/Animations/AnimationsOverview'; +import { AnimationsNotificationsDrawer } from '../../../../dist/esm/demos/Animations/AnimationsNotificationsDrawer'; +import { AnimationsCreateDatabaseForm } from '../../../../dist/esm/demos/Animations/AnimationsCreateDatabaseForm'; +import { GuidedTourProvider, useGuidedTour } from '../../../../dist/esm/demos/Animations/GuidedTourContext'; +import { AnimationsHeaderToolbar } from '../../../../dist/esm/demos/Animations/AnimationsHeaderToolbar'; +import { AnimationsStartTourModal } from '../../../../dist/esm/demos/Animations/AnimationsStartTourModal'; +import { AnimationsEndTourModal } from '../../../../dist/esm/demos/Animations/AnimationsEndTourModal'; import { applicationsData } from './ResourceTableData'; const mainContainerPageId = 'main-content-page-layout-default-nav'; @@ -203,10 +206,15 @@ const AnimationsPage: FunctionComponent = () => { ]); const [selectedTab, setSelectedTab] = useState(0); const [showForm, setShowForm] = useState(false); - const [showTourModal, setShowTourModal] = useState(true); + const [showStartTourModal, setShowStartTourModal] = useState(true); + const [showEndTourModal, setShowEndTourModal] = useState(false); const [activeItem, setActiveItem] = useState(0); const [activeGroup, setActiveGroup] = useState(null); - const { onStart, onFinish, renderTourStepElement, setCustomStepContent, tourStep } = useGuidedTour(); + const { onStart, onFinish, renderTourStepElement, setCustomStepContent, tourStep, isFinished } = useGuidedTour(); + const [windowWidth, setWindowWidth] = useState(); + const unObserver = useRef(null); + + const isMobile = windowWidth < 500; const addNotification = useCallback((showToast = true) => { setNotifications((prev) => [ @@ -225,36 +233,52 @@ const AnimationsPage: FunctionComponent = () => { }, []); useEffect(() => { - if (tourStep?.stepId === 'notificationBadge') { + if (tourStep?.stepId === 'toastNotifications') { setCustomStepContent( <> - Click Add notification. Watch for a new notification to arrive. + Click Add alert. In a moment, a new toast alert will appear. - The bell icon "rings" with a subtle rotation to quickly catch your as a message comes in. + Notice how it slides smoothly into view to draw your eye to critical information, and then slides out just + as smoothly once it expires. - ); } - if (tourStep?.stepId === 'toastNotifications') { + if (tourStep?.stepId === 'settingsButton') { setCustomStepContent( <> + {!isMobile ? ( + + Hover over the settings button. The cog icon rotates to show that it’s interactive. + + ) : null} - Click Add alert. In a moment, a new toast alert will appear. + {`Click ${isMobile ? 'the settings button' : 'it'} to see the new ripple effect we've added to all buttons`} + . + + ); + } + + if (tourStep?.stepId === 'notificationBadge') { + setCustomStepContent( + <> - Notice how it slides smoothly into view to draw your eye to critical information, and then slides out just - as smoothly once it expires. + Click Add notification. Watch for a new notification to arrive. - @@ -267,7 +291,41 @@ const AnimationsPage: FunctionComponent = () => { setSelectedTab(2); setShowForm(true); } - }, [tourStep?.stepId, setCustomStepContent, addNotification]); + }, [tourStep?.stepId, setCustomStepContent, addNotification, isMobile]); + + useEffect(() => { + setShowEndTourModal(isFinished); + }, [isFinished]); + + const measureRef = (ref: HTMLDivElement) => { + // Remove any previous observer + if (unObserver.current) { + unObserver.current(); + } + + if (!ref) { + return; + } + + const handleResize = () => setWindowWidth(ref.clientWidth); + + // Set size on initialization + handleResize(); + + const debounceResize = debounce(handleResize, 100); + + // Update graph size on resize events + unObserver.current = getResizeObserver(ref, debounceResize); + }; + + useEffect( + () => () => { + if (unObserver.current) { + unObserver.current(); + } + }, + [] + ); const startNotifications = () => { setTimeout(() => { @@ -304,8 +362,8 @@ const AnimationsPage: FunctionComponent = () => { firstTabbableItem?.focus(); }; - const closeTourModal = (startTour = false) => { - setShowTourModal(false); + const closeStartTourModal = (startTour = false) => { + setShowStartTourModal(false); startTour ? onStart() : startNotifications(); }; @@ -331,7 +389,7 @@ const AnimationsPage: FunctionComponent = () => { notifications={notifications} isDrawerExpanded={isDrawerExpanded} setIsDrawerExpanded={setIsDrawerExpanded} - onStartGuidedTour={() => setShowTourModal(true)} + onStartGuidedTour={() => setShowStartTourModal(true)} onEndGuidedTour={() => onFinish()} /> @@ -405,10 +463,19 @@ const AnimationsPage: FunctionComponent = () => { } mainContainerId={mainContainerPageId} > +
    {renderTourStepElement( 'toastNotifications', -
    +
    )} {notifications .filter((n) => n.isNew) @@ -473,7 +540,8 @@ const AnimationsPage: FunctionComponent = () => { )} )} - {showTourModal ? : null} + {showStartTourModal ? : null} + {showEndTourModal ? : null} ); }; From ae41f604a882104369e6af72fa91efab5c17971c Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Wed, 16 Jul 2025 13:04:53 -0400 Subject: [PATCH 13/17] animations guided tour: Add spotlight --- .../AnimationsCreateDatabaseForm.tsx | 4 +- .../Animations/AnimationsEndTourModal.tsx | 7 +- .../Animations/AnimationsHeaderToolbar.tsx | 39 +------ .../Animations/AnimationsStartTourModal.tsx | 2 +- .../demos/Animations/GuidedTourContext.tsx | 108 +++++++++--------- .../src/demos/Animations/Spotlight.tsx | 80 +++++++++++++ .../demos/Animations/examples/Animations.tsx | 59 +++++----- .../react-core/src/demos/Animations/types.ts | 2 + 8 files changed, 179 insertions(+), 122 deletions(-) create mode 100644 packages/react-core/src/demos/Animations/Spotlight.tsx diff --git a/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx b/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx index f64a840f2fd..e78dd5132b3 100644 --- a/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx +++ b/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx @@ -113,7 +113,7 @@ export const AnimationsCreateDatabaseForm: FunctionComponent = ({ onClose return renderTourStepElement( 'validationErrors', -
    + {actionCompleted && isSuccess ? ( @@ -244,7 +244,7 @@ export const AnimationsCreateDatabaseForm: FunctionComponent = ({ onClose )} - diff --git a/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx b/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx index f01dd0ad8d3..2de77f2608e 100644 --- a/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx +++ b/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx @@ -14,9 +14,7 @@ import { ToolbarContent } from '../..'; import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon'; -import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; -import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; import imgAvatar from '@patternfly/react-core/src/components/assets/avatarImg.svg'; import { NotificationType } from './types'; import { useGuidedTour } from './GuidedTourContext'; @@ -37,14 +35,11 @@ export const AnimationsHeaderToolbar: FunctionComponent = ({ onEndGuidedTour }) => { const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); - const [isKebabDropdownOpen, setIsKebabDropdownOpen] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [shouldNotifyNewNotification, setShouldNotifyNewNotification] = useState(false); const { renderTourStepElement, tourStep } = useGuidedTour(); const previousUnreadCountRef = useRef(notifications.filter((n) => !n.isRead).length); - const onKebabDropdownSelect = () => setIsKebabDropdownOpen(false); - const unreadNotificationCount = notifications.filter((n) => !n.isRead).length; useEffect(() => { @@ -71,6 +66,7 @@ export const AnimationsHeaderToolbar: FunctionComponent = ({ {renderTourStepElement( 'notificationBadge', setIsDrawerExpanded(!isDrawerExpanded)} aria-label="Notifications" @@ -85,6 +81,7 @@ export const AnimationsHeaderToolbar: FunctionComponent = ({ {renderTourStepElement( 'settingsButton',
    - - } - bodyContent={customStepContent || tourStep.content} - footerContent={ - - - Step {currentStep + 1}/{steps.length} - - - - - - - - - - - - - } - > - {child} - + <> + {tourStep.spotlightSelector ? ( + + ) : null} + + {tourStep.header} + { + // Had to add a close button here rather than using the showClose property to include the close button + // Using the provided close button requires the 'shouldClose' property to handle the close click, but it also + // gets called on a triggerRef click which we don't want since we ask the user to click the button in order + // to see the animation. I don't see how to distinguish between the close button click and the triggerRef click. + } +
    +
    + + } + bodyContent={customStepContent || tourStep.content} + footerContent={ + + + Step {currentStep + 1}/{steps.length} + + + + + + + + + + + + + } + > + {child} +
    + ); }, [tourStep, currentStep, steps, onNextStep, onPrevStep, onFinish, customStepContent, isMobile] diff --git a/packages/react-core/src/demos/Animations/Spotlight.tsx b/packages/react-core/src/demos/Animations/Spotlight.tsx new file mode 100644 index 00000000000..2840bda6c38 --- /dev/null +++ b/packages/react-core/src/demos/Animations/Spotlight.tsx @@ -0,0 +1,80 @@ +import { useEffect, useRef, useState } from 'react'; +import { debounce, getResizeObserver } from '../..'; + +const SpotlightBorderWidth = 3; +const SpotlightGap = 4; + +type BoundingClientRect = ClientRect | null; + +interface SpotlightProps { + selector: string; + resizeSelector?: string; +} + +const Spotlight: React.FC = ({ selector, resizeSelector }) => { + const [clientRect, setClientRect] = useState( + document.querySelector(selector)?.getBoundingClientRect() + ); + const unObserver = useRef(null); + + // if target element is a hidden one return null + const element = document.querySelector(selector); + + useEffect(() => { + if (!element) { + return; + } + + const handleResize = () => { + if (element) { + setClientRect(element.getBoundingClientRect()); + } + }; + + const debounceResize = debounce(handleResize, 100); + + // Update graph size on resize events + const resizeElement = resizeSelector ? document.querySelector(resizeSelector) || element : element; + unObserver.current = getResizeObserver(resizeElement, debounceResize); + + return () => { + if (unObserver.current) { + unObserver.current(); + unObserver.current = undefined; + } + }; + }, [element, resizeSelector]); + + useEffect( + () => () => { + if (unObserver.current) { + unObserver.current(); + unObserver.current = undefined; + } + }, + [] + ); + + if (!element) { + return null; + } + + const style: React.CSSProperties = clientRect + ? { + position: 'fixed', + top: clientRect.top - (SpotlightBorderWidth + SpotlightGap), + left: clientRect.left - (SpotlightBorderWidth + SpotlightGap), + height: clientRect.height + 2 * (SpotlightBorderWidth + SpotlightGap), + width: clientRect.width + 2 * (SpotlightBorderWidth + SpotlightGap), + borderWidth: 3, + borderStyle: 'solid', + borderColor: 'var(--pf-t--global--background--color--highlight--default)', + background: 'transparent', + pointerEvents: 'none' + } + : {}; + + return clientRect ?
    : null; +}; + +export default Spotlight; diff --git a/packages/react-core/src/demos/Animations/examples/Animations.tsx b/packages/react-core/src/demos/Animations/examples/Animations.tsx index eb266e1fc5a..71eea23d98d 100644 --- a/packages/react-core/src/demos/Animations/examples/Animations.tsx +++ b/packages/react-core/src/demos/Animations/examples/Animations.tsx @@ -63,14 +63,8 @@ export const GuidedTourSteps: GuidedTourStep[] = [ { stepId: 'settingsButton', header:
    Buttons: Settings
    , - content: ( - <> - - Hover over the settings button. The cog icon rotates to show that it’s interactive. - - Click it to see the new ripple effect we've added to all buttons. - - ) + content: '===== This content is customized ======', + spotlightSelector: '#settings-button' }, { stepId: 'navToggle', @@ -83,12 +77,14 @@ export const GuidedTourSteps: GuidedTourStep[] = [ will happen next. - ) + ), + spotlightSelector: '#nav-toggle' }, { stepId: 'notificationBadge', header:
    Buttons: Notification badge
    , - content: '===== This content is customized ======' + content: '===== This content is customized ======', + spotlightSelector: '#notification-badge' }, { stepId: 'tabs', @@ -99,7 +95,8 @@ export const GuidedTourSteps: GuidedTourStep[] = [ Click between the different tabs and watch how the active tab indicator smoothly slides to your selection, providing clear feedback on your location. - ) + ), + spotlightSelector: '#tabs' }, { stepId: 'skeletonLoader', @@ -112,7 +109,8 @@ export const GuidedTourSteps: GuidedTourStep[] = [ scenes. - ) + ), + spotlightSelector: '#skeleton-table' }, { stepId: 'expandableComponents', @@ -125,8 +123,10 @@ export const GuidedTourSteps: GuidedTourStep[] = [ Notice how the hidden information smoothly fades and slides into place. Click again to collapse it and see the reverse animation. + Reduced-motion users will only see the fade, not the sliding motion. - ) + ), + spotlightSelector: '#expand-toggle-1' }, { stepId: 'validationErrors', @@ -134,12 +134,14 @@ export const GuidedTourSteps: GuidedTourStep[] = [ content: ( <> - Click the Submit button while fields are empty to trigger an error. Watch the input fields - jiggle from side to side, drawing your attention to issues that need fixing. + Click Submit while fields are empty to trigger an error. Watch the input fields jiggle from + side to side, drawing your attention to issues that need fixing. Reduced-motion users will only see the fade, not the jiggle. - ) + ), + spotlightSelector: '#create-database-submit', + spotlightResizeSelector: '#create-database-form' // }, // { // stepId: 'progressStepper', @@ -260,8 +262,7 @@ const AnimationsPage: FunctionComponent = () => { ) : null} - {`Click ${isMobile ? 'the settings button' : 'it'} to see the new ripple effect we've added to all buttons`} - . + {`Click ${isMobile ? 'the settings button' : 'it'} to see the new ripple effect we've added to all buttons.`} ); @@ -274,7 +275,7 @@ const AnimationsPage: FunctionComponent = () => { Click Add notification. Watch for a new notification to arrive. - The bell icon "rings" with a subtle rotation to quickly catch your as a message comes in. + The bell icon "rings" with a subtle rotation to quickly catch your attention as a message comes in.