Skip to content

Commit

Permalink
fix(react): Support for adding css classes via className in Ionic R…
Browse files Browse the repository at this point in the history
…eact components (#18231)

* fix(react): adding classname to react props

* fix(react): updating rtl to latest to fix ts error

* fix(react): changes to support className
  • Loading branch information
Ely Lucas authored and jthoms1 committed May 8, 2019
1 parent 9c65d5d commit 7bb6a8e
Show file tree
Hide file tree
Showing 17 changed files with 278 additions and 148 deletions.
2 changes: 1 addition & 1 deletion react/package.json
Expand Up @@ -48,7 +48,7 @@
"react-dom": "^16.7.0",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-testing-library": "^5.5.3",
"react-testing-library": "^7.0.0",
"ts-jest": "^23.10.5",
"typescript": "^3.2.2"
},
Expand Down
3 changes: 2 additions & 1 deletion react/src/components/IonActionSheet.tsx
@@ -1,7 +1,8 @@
import { Components } from '@ionic/core';
import { createOverlayComponent } from './createOverlayComponent';
import { ReactProps } from './ReactProps';

export type ActionSheetOptions = Components.IonActionSheetAttributes;

const IonActionSheet = createOverlayComponent<ActionSheetOptions, HTMLIonActionSheetElement, HTMLIonActionSheetControllerElement>('ion-action-sheet', 'ion-action-sheet-controller')
const IonActionSheet = createOverlayComponent<ActionSheetOptions & ReactProps, HTMLIonActionSheetElement, HTMLIonActionSheetControllerElement>('ion-action-sheet', 'ion-action-sheet-controller')
export default IonActionSheet;
3 changes: 2 additions & 1 deletion react/src/components/IonAlert.tsx
@@ -1,7 +1,8 @@
import { Components } from '@ionic/core';
import { createControllerComponent } from './createControllerComponent';
import { ReactProps } from './ReactProps';

export type AlertOptions = Components.IonAlertAttributes;

const IonAlert = createControllerComponent<AlertOptions, HTMLIonAlertElement, HTMLIonAlertControllerElement>('ion-alert', 'ion-alert-controller')
const IonAlert = createControllerComponent<AlertOptions & ReactProps, HTMLIonAlertElement, HTMLIonAlertControllerElement>('ion-alert', 'ion-alert-controller')
export default IonAlert;
3 changes: 2 additions & 1 deletion react/src/components/IonLoading.tsx
@@ -1,7 +1,8 @@
import { Components } from '@ionic/core';
import { createControllerComponent } from './createControllerComponent';
import { ReactProps } from './ReactProps';

export type LoadingOptions = Components.IonLoadingAttributes;

const IonLoading = createControllerComponent<LoadingOptions, HTMLIonLoadingElement, HTMLIonLoadingControllerElement>('ion-loading', 'ion-loading-controller')
const IonLoading = createControllerComponent<LoadingOptions & ReactProps, HTMLIonLoadingElement, HTMLIonLoadingControllerElement>('ion-loading', 'ion-loading-controller')
export default IonLoading;
3 changes: 2 additions & 1 deletion react/src/components/IonModal.tsx
@@ -1,10 +1,11 @@
import { Components } from '@ionic/core';
import { createOverlayComponent } from './createOverlayComponent';
import { Omit } from '../types';
import { ReactProps } from './ReactProps';

export type ModalOptions = Omit<Components.IonModalAttributes, 'component' | 'componentProps'> & {
children: React.ReactNode;
};

const IonModal = createOverlayComponent<ModalOptions, HTMLIonModalElement, HTMLIonModalControllerElement>('ion-modal', 'ion-modal-controller')
const IonModal = createOverlayComponent<ModalOptions & ReactProps, HTMLIonModalElement, HTMLIonModalControllerElement>('ion-modal', 'ion-modal-controller')
export default IonModal;
3 changes: 2 additions & 1 deletion react/src/components/IonPopover.tsx
@@ -1,10 +1,11 @@
import { Components } from '@ionic/core';
import { createOverlayComponent } from './createOverlayComponent';
import { Omit } from '../types';
import { ReactProps } from './ReactProps';

export type PopoverOptions = Omit<Components.IonPopoverAttributes, 'component' | 'componentProps'> & {
children: React.ReactNode;
};

const IonPopover = createOverlayComponent<PopoverOptions, HTMLIonPopoverElement, HTMLIonPopoverControllerElement>('ion-popover', 'ion-popover-controller')
const IonPopover = createOverlayComponent<PopoverOptions & ReactProps, HTMLIonPopoverElement, HTMLIonPopoverControllerElement>('ion-popover', 'ion-popover-controller')
export default IonPopover;
3 changes: 2 additions & 1 deletion react/src/components/IonToast.tsx
@@ -1,7 +1,8 @@
import { Components } from '@ionic/core';
import { createControllerComponent } from './createControllerComponent';
import { ReactProps } from './ReactProps';

export type ToastOptions = Components.IonToastAttributes;

const IonToast = createControllerComponent<ToastOptions, HTMLIonToastElement, HTMLIonToastControllerElement>('ion-toast', 'ion-toast-controller')
const IonToast = createControllerComponent<ToastOptions & ReactProps, HTMLIonToastElement, HTMLIonToastControllerElement>('ion-toast', 'ion-toast-controller')
export default IonToast;
3 changes: 3 additions & 0 deletions react/src/components/ReactProps.ts
@@ -0,0 +1,3 @@
export interface ReactProps {
className?: string;
}
88 changes: 87 additions & 1 deletion react/src/components/__tests__/createComponent.spec.tsx
@@ -1,7 +1,8 @@
import React from 'react';
import { Components } from '@ionic/core';
import { createReactComponent } from '../createComponent';
import { render, fireEvent, cleanup } from 'react-testing-library';
import { render, fireEvent, cleanup, RenderResult } from 'react-testing-library';
import { IonButton } from '../index';

afterEach(cleanup);

Expand Down Expand Up @@ -47,3 +48,88 @@ describe('createComponent - ref', () => {
expect(ionButtonRef.current).toEqual(ionButtonItem);
});
});

describe('when working with css classes', () => {
const myClass = 'my-class'
const myClass2 = 'my-class2'
const customClass = 'custom-class';

describe('when a class is added to className', () => {
let renderResult: RenderResult;
let button: HTMLElement;

beforeEach(() => {
renderResult = render(
<IonButton className={myClass}>
Hello!
</IonButton>
);
button = renderResult.getByText(/Hello/);
});

it('then it should be in the class list', () => {
expect(button.classList.contains(myClass)).toBeTruthy();
});

it('when a class is added to class list outside of React, then that class should still be in class list when rendered again', () => {
button.classList.add(customClass);
expect(button.classList.contains(customClass)).toBeTruthy();
renderResult.rerender(
<IonButton className={myClass}>
Hello!
</IonButton>
);
expect(button.classList.contains(customClass)).toBeTruthy();
});
});

describe('when multiple classes are added to className', () => {
let renderResult: RenderResult;
let button: HTMLElement;

beforeEach(() => {
renderResult = render(
<IonButton className={myClass + ' ' + myClass2}>
Hello!
</IonButton>
);
button = renderResult.getByText(/Hello/);
});

it('then both classes should be in class list', () => {
expect(button.classList.contains(myClass)).toBeTruthy();
expect(button.classList.contains(myClass2)).toBeTruthy();
});

it('when one of the classes is removed, then only the remaining class should be in class list', () => {
expect(button.classList.contains(myClass)).toBeTruthy();
expect(button.classList.contains(myClass2)).toBeTruthy();

renderResult.rerender(
<IonButton className={myClass}>
Hello!
</IonButton>
);

expect(button.classList.contains(myClass)).toBeTruthy();
expect(button.classList.contains(myClass2)).toBeFalsy();
});

it('when a custom class is added outside of React and one of the classes is removed, then only the remaining class and the custom class should be in class list', () => {
button.classList.add(customClass);
expect(button.classList.contains(myClass)).toBeTruthy();
expect(button.classList.contains(myClass2)).toBeTruthy();
expect(button.classList.contains(customClass)).toBeTruthy();

renderResult.rerender(
<IonButton className={myClass}>
Hello!
</IonButton>
);

expect(button.classList.contains(myClass)).toBeTruthy();
expect(button.classList.contains(myClass)).toBeTruthy();
expect(button.classList.contains(myClass2)).toBeFalsy();
});
})
});
Expand Up @@ -68,7 +68,7 @@ describe('createControllerComponent - events', () => {
duration: 2000,
children: 'ButtonNameA',
onIonLoadingDidDismiss: onDidDismiss
});
}, expect.any(Object));
});

test('should dismiss component on hiding', async () => {
Expand Down
Expand Up @@ -65,7 +65,7 @@ describe('createOverlayComponent - events', () => {
expect(attachEventPropsSpy).toHaveBeenCalledWith(element, {
buttons: [],
onIonActionSheetDidDismiss: onDidDismiss
});
}, expect.any(Object));
});

test('should dismiss component on hiding', async () => {
Expand Down
19 changes: 9 additions & 10 deletions react/src/components/createComponent.tsx
Expand Up @@ -2,24 +2,24 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { dashToPascalCase, attachEventProps } from './utils';

export function createReactComponent<T extends object, E>(tagName: string) {
export function createReactComponent<PropType, ElementType>(tagName: string) {
const displayName = dashToPascalCase(tagName);

type IonicReactInternalProps = {
forwardedRef?: React.RefObject<E>;
forwardedRef?: React.RefObject<ElementType>;
children?: React.ReactNode;
}
type InternalProps = T & IonicReactInternalProps;
type InternalProps = PropType & IonicReactInternalProps;

type IonicReactExternalProps = {
ref?: React.RefObject<E>;
ref?: React.RefObject<ElementType>;
children?: React.ReactNode;
}

class ReactComponent extends React.Component<InternalProps> {
componentRef: React.RefObject<E>;
componentRef: React.RefObject<ElementType>;

constructor(props: T & IonicReactInternalProps) {
constructor(props: PropType & IonicReactInternalProps) {
super(props);
this.componentRef = React.createRef();
}
Expand All @@ -34,8 +34,7 @@ export function createReactComponent<T extends object, E>(tagName: string) {

componentWillReceiveProps(props: InternalProps) {
const node = ReactDOM.findDOMNode(this) as HTMLElement;

attachEventProps(node, props);
attachEventProps(node, props, this.props);
}

render() {
Expand All @@ -52,10 +51,10 @@ export function createReactComponent<T extends object, E>(tagName: string) {
}
}

function forwardRef(props: InternalProps, ref: React.RefObject<E>) {
function forwardRef(props: InternalProps, ref: React.RefObject<ElementType>) {
return <ReactComponent {...props} forwardedRef={ref} />;
}
forwardRef.displayName = displayName;

return React.forwardRef<E, T & IonicReactExternalProps>(forwardRef);
return React.forwardRef<ElementType, PropType & IonicReactExternalProps>(forwardRef);
}
2 changes: 1 addition & 1 deletion react/src/components/createControllerComponent.tsx
Expand Up @@ -45,7 +45,7 @@ export function createControllerComponent<T extends object, E extends OverlayCom
}

this.element = await this.controllerElement.create(elementProps);
attachEventProps(this.element, elementProps);
attachEventProps(this.element, elementProps, prevProps);

await this.element.present();
}
Expand Down
2 changes: 1 addition & 1 deletion react/src/components/createOverlayComponent.tsx
Expand Up @@ -52,7 +52,7 @@ export function createOverlayComponent<T extends object, E extends OverlayCompon
component: this.el,
componentProps: {}
});
attachEventProps(this.element, elementProps);
attachEventProps(this.element, elementProps, prevProps);

await this.element.present();
}
Expand Down

0 comments on commit 7bb6a8e

Please sign in to comment.