Skip to content

Commit a678fd8

Browse files
OpenStaxClaudeclaudeCopilotRoyEJohnson
authored
Phase 7.3c: Migrate App Components - Layout & Common UI to Plain CSS (#3061)
* Phase 7.3c: Migrate App Components - Layout & Common UI to Plain CSS Migrated 7 app-level components from styled-components to plain CSS: **Components Migrated:** - Layout.tsx - Removed createGlobalStyle for MathJax, moved to CSS - Loader.tsx - Converted all keyframe animations and styles to plain CSS - Dropdown.tsx - Removed styled() wrappers, moved inline styles to CSS - AllOrNone.tsx - Full migration with new AllOrNone.css - Details.tsx - Removed styled() wrappers from icon components - Checkbox.tsx - Removed styled() wrapper, now plain React component - PageTitleConfirmation.tsx - Migrated from connect() to useSelector hooks, removed styled-components **Files Changed:** - 9 .tsx files migrated (removed styled-components imports) - 2 new .css files created (AllOrNone.css, Loader.css) - 2 existing .css files updated (Layout.css, Dropdown.css) **Migration Approach:** - Followed simplified in-place migration pattern from PLAIN_CSS_MIGRATION_LEARNINGS.md - Used CSS variables for dynamic values (delay, width, etc.) - Converted Redux connect() HOC to useSelector hooks pattern - Preserved all component functionality and TypeScript types - No breaking changes to component APIs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix review issues: restore styled() wrappers and add fadeIn keyframe Addresses code review feedback from PR #3061: 1. Restored styled() wrappers for backward compatibility: - DropdownToggle, Dropdown: Wrapped with styled() to support component selectors in Filters.tsx - AllOrNone: Wrapped with styled() to support component selectors in ColorFilter/ChapterFilter - Checkbox: Wrapped with styled() to support component selectors in ColorFilter/ChapterFilter These components still use plain CSS for all styling (in .css files), but the styled() wrapper maintains compatibility with existing component selector usage in other files that haven't been migrated yet. 2. Added fadeIn keyframe to Loader.css: - Defined @Keyframes fadeIn (0% opacity: 0; 100% opacity: 1) - Previously relied on NavBar/NavBar.css, but this creates fragile cross-component dependency - Now self-contained to avoid issues if styles are code-split or refactored Fixes test failures where DropdownToggle.cloneElement was receiving undefined component. 🤖 Generated with [Claude Code](https://claude.com/claude-code) No need for default value Co-Authored-By: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Snaps Co-Authored-By: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix CSS loading issue Co-Authored-By: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Remove unused separate disconnected PageTitleConfirmation Also comment some CSS lint exceptions * Remove deprecated SFC from Layout * Copilot suggestions Namespacing @Keyframes to avoid unintended coupling Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Print button layout fix (from 7.1b) * Reduce specificity of print-opt-wrapper * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Roy Johnson <roy.e.johnson@rice.edu>
1 parent 2f8b4ed commit a678fd8

35 files changed

Lines changed: 810 additions & 1060 deletions

src/app/components/AllOrNone.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* AllOrNone component styles */
2+
3+
.all-or-none {
4+
height: 2rem;
5+
display: flex;
6+
flex-direction: row;
7+
align-items: center;
8+
font-size: 1.4rem;
9+
overflow: visible;
10+
}
11+
12+
.all-or-none .button-link {
13+
font-size: 1.4rem;
14+
overflow: visible;
15+
}
16+
17+
.all-or-none span {
18+
padding: 0 1rem;
19+
}

src/app/components/AllOrNone.tsx

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import React from 'react';
2+
import classNames from 'classnames';
23
import { FormattedMessage } from 'react-intl';
34
import styled from 'styled-components/macro';
4-
import { ButtonLink as ButtonLinkBase } from './Button';
5-
6-
// Wrap with styled() to make ButtonLink compatible with component selectors
7-
const ButtonLink = styled(ButtonLinkBase)``;
5+
import { ButtonLink } from './Button';
6+
import './AllOrNone.css';
87

98
interface Props {
109
className?: string;
@@ -13,29 +12,21 @@ interface Props {
1312
disabled?: boolean;
1413
}
1514

16-
const AllOrNone = ({className, onAll, onNone, disabled}: Props) => <div className={className}>
17-
<FormattedMessage id='i18n:highlighting:filters:all'>
18-
{(msg) => <ButtonLink disabled={disabled} decorated onClick={onAll}>{msg}</ButtonLink>}
19-
</FormattedMessage>
20-
<span aria-hidden="true">|</span>
21-
<FormattedMessage id='i18n:highlighting:filters:none'>
22-
{(msg) => <ButtonLink disabled={disabled} decorated onClick={onNone}>{msg}</ButtonLink>}
23-
</FormattedMessage>
24-
</div>;
25-
26-
export default styled(AllOrNone)`
27-
&,
28-
${ButtonLink} {
29-
font-size: 1.4rem;
30-
overflow: visible;
31-
}
32-
33-
height: 2rem;
34-
display: flex;
35-
flex-direction: row;
36-
align-items: center;
15+
// Plain React component for AllOrNone
16+
function AllOrNoneBase({ className, onAll, onNone, disabled }: Props) {
17+
return (
18+
<div className={classNames('all-or-none', className)}>
19+
<FormattedMessage id='i18n:highlighting:filters:all'>
20+
{(msg) => <ButtonLink disabled={disabled} decorated onClick={onAll}>{msg}</ButtonLink>}
21+
</FormattedMessage>
22+
<span aria-hidden="true">|</span>
23+
<FormattedMessage id='i18n:highlighting:filters:none'>
24+
{(msg) => <ButtonLink disabled={disabled} decorated onClick={onNone}>{msg}</ButtonLink>}
25+
</FormattedMessage>
26+
</div>
27+
);
28+
}
3729

38-
span {
39-
padding: 0 1rem;
40-
}
41-
`;
30+
// Wrap with styled() for backward compatibility with component selectors
31+
// Styles are now in AllOrNone.css, but this wrapper maintains selector compatibility
32+
export default styled(AllOrNoneBase)``;

src/app/components/Details.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React from 'react';
22
import classNames from 'classnames';
3-
import styled from 'styled-components/macro';
43
import { HTMLDetailsElement } from '@openstax/types/lib.dom';
54
import '../../polyfill/details';
65
// Note: Details.css is imported globally from src/app/index.tsx to ensure consistent
@@ -15,10 +14,8 @@ interface IconProps extends React.SVGAttributes<SVGSVGElement> {
1514
/**
1615
* Expand icon (caret-right) for Details component.
1716
* SVG path from Font Awesome Free (https://fontawesome.com - MIT License)
18-
*
19-
* Note: Wrapped with styled() to enable styled-components component selector references
2017
*/
21-
function ExpandIconBase({ className, ...props }: IconProps) {
18+
export function ExpandIcon({ className, ...props }: IconProps) {
2219
return (
2320
<svg
2421
className={classNames('details-expand-icon', className)}
@@ -34,15 +31,11 @@ function ExpandIconBase({ className, ...props }: IconProps) {
3431
);
3532
}
3633

37-
export const ExpandIcon = styled(ExpandIconBase)``;
38-
3934
/**
4035
* Collapse icon (caret-down) for Details component.
4136
* SVG path from Font Awesome Free (https://fontawesome.com - MIT License)
42-
*
43-
* Note: Wrapped with styled() to enable styled-components component selector references
4437
*/
45-
function CollapseIconBase({ className, ...props }: IconProps) {
38+
export function CollapseIcon({ className, ...props }: IconProps) {
4639
return (
4740
<svg
4841
className={classNames('details-collapse-icon', className)}
@@ -58,8 +51,6 @@ function CollapseIconBase({ className, ...props }: IconProps) {
5851
);
5952
}
6053

61-
export const CollapseIcon = styled(CollapseIconBase)``;
62-
6354
export function Summary({
6455
children,
6556
className,

src/app/components/Dropdown.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
}
1212
}
1313

14+
/* Base Dropdown wrapper styles */
15+
.dropdown-wrapper {
16+
overflow: visible;
17+
position: relative;
18+
}
19+
1420
/* DropdownToggle styles */
1521
.dropdown-toggle {
1622
cursor: pointer;
@@ -71,8 +77,10 @@
7177
.dropdown-list li button:focus,
7278
.dropdown-list li a:focus {
7379
background: var(--dropdown-form-border, #d5d5d5); /* theme.color.neutral.formBorder */
80+
/* stylelint-disable declaration-block-no-duplicate-properties -- different browsers */
7481
outline: 0.2rem auto Highlight;
7582
outline: 0.2rem auto -webkit-focus-ring-color;
83+
/* stylelint-enable declaration-block-no-duplicate-properties */
7684
}
7785

7886
/* TabTransparentDropdown focus-within behavior */

src/app/components/Dropdown.tsx

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,11 @@ function TrappingDropdownList(props: React.MenuHTMLAttributes<HTMLMenuElement>)
189189
);
190190
}
191191

192-
// Plain React component for DropdownList, but wrapped with styled() for backward compatibility
193-
const DropdownListBase = ({ className, ...props }: React.MenuHTMLAttributes<HTMLMenuElement>) => {
192+
// Plain React component for DropdownList
193+
export const DropdownList = ({ className, ...props }: React.MenuHTMLAttributes<HTMLMenuElement>) => {
194194
return <TrappingDropdownList className={classNames('dropdown-list', className)} {...props} />;
195195
};
196196

197-
// Wrap with styled() for backward compatibility with component selectors
198-
export const DropdownList = styled(DropdownListBase)``;
199-
200197
interface DropdownItemProps {
201198
message: string;
202199
ariaMessage?: string;
@@ -263,15 +260,13 @@ export type TabHiddenDropdownProps = CommonDropdownProps & (Props | Props & Cont
263260

264261
export type DropdownProps = TabTransparentDropdownProps | TabHiddenDropdownProps;
265262

266-
const DropdownBase = React.forwardRef<HTMLElement, DropdownProps>(({transparentTab, ...props}, ref) =>
267-
transparentTab !== false
268-
? <TabTransparentDropdown ref={ref} {...props} />
269-
: <TabHiddenDropDown ref={ref} {...props} />
270-
);
263+
// Plain React component for Dropdown, but wrapped with styled() for backward compatibility
264+
const DropdownBase = React.forwardRef<HTMLElement, DropdownProps>(({transparentTab, className, ...props}, ref) => {
265+
const Component = transparentTab !== false ? TabTransparentDropdown : TabHiddenDropDown;
266+
return <Component ref={ref} className={classNames('dropdown-wrapper', className)} {...props} />;
267+
});
271268

272-
const Dropdown = styled(DropdownBase)<DropdownProps>`
273-
overflow: visible;
274-
position: relative;
275-
`;
269+
// Wrap with styled() for backward compatibility with component selectors
270+
const Dropdown = styled(DropdownBase)``;
276271

277272
export default Dropdown;

src/app/components/Layout.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
/* Layout component styles */
22

3+
/* MathJax help background z-index
4+
* IMPORTANT: This value is derived from theme.zIndex.navbar + 1
5+
* navbar is at position 7 in the zIndex array: (7) * 10 = 70
6+
* Therefore: 70 + 1 = 71
7+
* If theme.zIndex array changes, this value MUST be updated accordingly.
8+
*/
9+
/* stylelint-disable selector-type-no-unknown */
10+
mjx-help-background {
11+
z-index: 71;
12+
}
13+
/* stylelint-enable selector-type-no-unknown */
14+
315
.layout-body {
416
width: 100%;
517
padding: 0 var(--layout-padding-desktop, 3.2rem);

src/app/components/Layout.tsx

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,27 @@
1-
import React, { SFC } from 'react';
1+
import React from 'react';
22
import classNames from 'classnames';
3-
import { createGlobalStyle } from 'styled-components/macro';
43
import ErrorBoundary from '../errors/components/ErrorBoundary';
54
import ErrorModal from '../errors/components/ErrorModal';
6-
import theme from '../theme';
75
import AccessibilityButtonsWrapper from './AccessibilityButtonsWrapper';
86
import NavBar from './NavBar';
97
import OnEsc from './OnEsc';
108
import PageTitleConfirmation from './PageTitleConfirmation';
119
import { layoutPadding } from './Layout.constants';
1210
import './Layout.css';
1311

14-
const MathJaxStyles = createGlobalStyle`
15-
mjx-help-background {
16-
z-index: ${theme.zIndex.navbar + 1};
17-
}
18-
`;
19-
20-
const Layout: SFC = ({ children }) => <AccessibilityButtonsWrapper>
21-
<MathJaxStyles />
22-
<NavBar />
23-
<OnEsc />
24-
<PageTitleConfirmation />
25-
<ErrorModal />
26-
<ErrorBoundary>
27-
{children}
28-
</ErrorBoundary>
29-
</AccessibilityButtonsWrapper>;
12+
export default function Layout({ children }: React.PropsWithChildren<{}>) {
13+
return (
14+
<AccessibilityButtonsWrapper>
15+
<NavBar />
16+
<OnEsc />
17+
<PageTitleConfirmation />
18+
<ErrorModal />
19+
<ErrorBoundary>
20+
{children}
21+
</ErrorBoundary>
22+
</AccessibilityButtonsWrapper>
23+
);
24+
}
3025

3126
// Export legacy styled-components fragment for backward compatibility
3227
export { wrapperPadding } from './Layout.legacy';
@@ -49,5 +44,3 @@ export const LayoutBody = ({
4944
{children}
5045
</div>
5146
);
52-
53-
export default Layout;

src/app/components/Loader.css

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/* Keyframe animations for loader */
2+
3+
@keyframes loaderFadeIn {
4+
0% {
5+
opacity: 0;
6+
}
7+
8+
100% {
9+
opacity: 1;
10+
}
11+
}
12+
13+
@keyframes loadSvg {
14+
from {
15+
transform: scale(0);
16+
}
17+
18+
to {
19+
opacity: 1;
20+
transform: scale(1);
21+
}
22+
}
23+
24+
@keyframes moveGreen {
25+
from {
26+
transform: matrix(1, 0, 0, 1, 0, 0);
27+
}
28+
29+
to {
30+
transform: matrix(0.965926, -0.258819, 0.258819, 0.965926, 0, -18);
31+
}
32+
}
33+
34+
@keyframes moveOrange {
35+
from {
36+
transform: matrix(1, 0, 0, 1, 0, 0);
37+
}
38+
39+
to {
40+
transform: matrix(0.994522, 0.104528, -0.104528, 0.994522, 0, -14);
41+
}
42+
}
43+
44+
@keyframes moveGray {
45+
from {
46+
transform: matrix(1, 0, 0, 1, 0, 0);
47+
}
48+
49+
to {
50+
transform: matrix(0.99863, 0.052336, -0.052336, 0.99863, 0, -8);
51+
}
52+
}
53+
54+
@keyframes moveYellow {
55+
from {
56+
transform: matrix(1, 0, 0, 1, 0, 0);
57+
}
58+
59+
to {
60+
transform: matrix(1, 0, 0, 1, 0, -8);
61+
}
62+
}
63+
64+
@keyframes moveBlue {
65+
from {
66+
transform: matrix(1, 0, 0, 1, 0, 0);
67+
}
68+
69+
to {
70+
transform: matrix(0.99863, 0.052336, -0.052336, 0.99863, 0, -4);
71+
}
72+
}
73+
74+
/* Loader icon component styles */
75+
.loading-icon {
76+
/* stylelint-disable property-no-unknown -- `enable-background` is an SVG presentation attribute */
77+
enable-background: new 0 0 57.6 39.1;
78+
/* stylelint-enable property-no-unknown */
79+
animation: loadSvg 0.3s forwards;
80+
animation-delay: var(--loader-delay, 300ms);
81+
display: block;
82+
height: 100%;
83+
margin: 0 auto;
84+
opacity: 0;
85+
position: relative;
86+
transform: scale(0);
87+
transform-origin: center;
88+
width: var(--loader-width, 5rem);
89+
}
90+
91+
.loading-icon * {
92+
animation: loaderFadeIn 0.3s forwards;
93+
}
94+
95+
.loading-icon .os-green,
96+
.loading-icon .os-orange,
97+
.loading-icon .os-gray,
98+
.loading-icon .os-yellow,
99+
.loading-icon .os-blue {
100+
animation-delay: 0.5s;
101+
}
102+
103+
.loading-icon .os-green {
104+
animation:
105+
moveGreen 0.6s cubic-bezier(0.81, 0.41, 0.13, 0.74) 0.5s
106+
infinite alternate;
107+
transform-origin: center left;
108+
}
109+
110+
.loading-icon .os-orange {
111+
animation:
112+
moveOrange 0.6s cubic-bezier(0.81, 0.41, 0.13, 0.74) 0.5s
113+
infinite alternate;
114+
transform-origin: center right;
115+
}
116+
117+
.loading-icon .os-gray {
118+
animation:
119+
moveGray 0.6s cubic-bezier(0.81, 0.41, 0.13, 0.74) 0.5s
120+
infinite alternate;
121+
transform-origin: center right;
122+
}
123+
124+
.loading-icon .os-yellow {
125+
animation:
126+
moveYellow 0.6s cubic-bezier(0.81, 0.41, 0.13, 0.74) 0.6s
127+
infinite alternate;
128+
transform-origin: center right;
129+
}
130+
131+
.loading-icon .os-blue {
132+
animation:
133+
moveBlue 0.6s cubic-bezier(0.81, 0.41, 0.13, 0.74) 0.5s
134+
infinite alternate;
135+
transform-origin: center;
136+
}

0 commit comments

Comments
 (0)