Ionic CSS Style Off when Deployed to iOS but fine everywhere else - css

I am not able to get my "Messages" Drawer to display in safari when in mobile, but it is displaying just fine in Chrome. Below are images of what it looks like in the 2 browsers. I assume this is a CSS issue and that the bottom option bar is covering the button in Safari, but I am not sure what needs to change to fix this issue?
I am noticing it has to do with the height and bottom values in the css code, but not sure exactly what to change it to to work in both web browsers?
Safari
Chrome
Here is the code for the Message Drawer
import { createGesture, IonButton, IonCard } from "#ionic/react";
/* Core CSS required for Ionic components to work properly */
import "#ionic/react/css/core.css";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import React, { useEffect, useRef, useState } from "react";
import { useGetThreadPageByIdQuery } from "../../../graphql/generated/component";
import { Request_Participant_Status_Value_Enum } from "../../../graphql/generated/graphql";
import ThreadMessages from "../../Messages/List";
import styles from "./styles.module.css";
let MessageWrapper = dynamic(
() => import("../../../components/Threads/MessageBox"),
{ ssr: false }
);
interface refType {
dataset: {
open: string;
};
style: {
transition: string;
transform: string;
};
}
const MessageDrawer = () => {
let drawerRef = useRef();
let router = useRouter();
let { threadID } = router.query;
let messageContainerRef = useRef(null);
let { data, loading } = useGetThreadPageByIdQuery({
variables: { thread_id: threadID },
nextFetchPolicy: "cache-and-network",
});
let scrollToLastMessage = () => {
let timer = setTimeout(() => {
messageContainerRef.current?.scrollIntoView({ beavior: "smooth" });
console.log("This will run after 1 second!");
console.log("message last scroll", data);
}, 500);
return () => clearTimeout(timer);
};
let [initialScrollCompleted, setInitialScrollComplete] =
useState<boolean>(false);
useEffect(() => {
if (initialScrollCompleted) {
return;
}
if (data) {
scrollToLastMessage();
return setInitialScrollComplete(true);
}
}, [loading, data]);
useEffect(() => {
let c: HTMLIonCardElement = drawerRef.current;
c.dataset.open = "false";
let gesture = createGesture({
el: c,
gestureName: "my-swipe",
direction: "y",
onMove: (event) => {
if (event.deltaY < -300) return;
// closing with a downward swipe
if (event.deltaY > 20) {
c.style.transform = "";
c.dataset.open = "false";
return;
}
c.style.transform = `translateY(${event.deltaY}px)`;
},
onEnd: (event) => {
c.style.transition = ".5s ease-out";
if (event.deltaY < -30 && c.dataset.open !== "true") {
c.style.transform = `translateY(${-62}vh) `;
c.dataset.open = "true";
console.log("in on end");
}
},
});
// enable the gesture for the item
gesture.enable(true);
}, []);
let toggleDrawer = () => {
let c: HTMLIonCardElement = drawerRef.current;
if (c) {
if (c.dataset.open === "true") {
c.style.transition = ".5s ease-out";
c.style.transform = "";
c.dataset.open = "false";
} else {
c.style.transition = ".5s ease-in";
c.style.transform = `translateY(${-62}vh) `;
c.dataset.open = "true";
}
}
};
return (
<IonCard className={styles.bottomdrawer} ref={drawerRef}>
<div className={styles.toggleMessage}>
<IonButton className={styles.toggleMessagebtn} onClick={toggleDrawer}>
<ion-icon
class={styles.icon}
slot="start"
src="./assets/dashboard/icons/messages_tab.svg"
/>
<ion-label>Messages</ion-label>
</IonButton>
</div>
<div className={styles.messageContent}>
<ion-list>
{/* <ion-item slot="header" class={styles.item}>
<ion-icon
class={styles.icon}
slot="start"
color="dark"
src="./assets/dashboard/icons/messages_tab.svg"
/>
<ion-label>Messages</ion-label>
</ion-item> */}
<div
slot="content"
className={`${styles.list} ${styles.commentsContainer}`}
>
<ThreadMessages threadID={threadID as string} />
<div
className={`${styles.messageContainerEnd}`}
ref={messageContainerRef}
/>
</div>
</ion-list>
<div className={styles.messageTypeContainer}>
{!data?.thread_by_pk?.request && data?.thread_by_pk?.breakdown ? (
<MessageWrapper
participants={data?.thread_by_pk.participants}
thread_id={threadID as string}
/>
) : null}
{!data?.thread_by_pk
?.request ? null : data?.thread_by_pk?.request?.request_participants?.find(
(request_participant) =>
request_participant.status ===
Request_Participant_Status_Value_Enum.Canceled
) ? null : (
<MessageWrapper
participants={data?.thread_by_pk.participants}
thread_id={threadID as string}
/>
)}
</div>
</div>
</IonCard>
);
};
export default MessageDrawer;
Here is the CSS Code for the Message Drawer.
.bottomdrawer {
position: fixed;
right: 0;
left: 0;
bottom: -71vh;
height: 74vh;
max-height: 100%;
border-radius: 10px 10px 0 0px;
top: auto;
margin: 0;
padding: 0;
-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
overflow: hidden;
width: calc(100% - 30px);
margin: 0 auto;
z-index: 9999;
}
.bottomdrawer[data-open="true"] {
bottom: -79vh;

I suggest placing it in the IonFooter component, this way it can be sticky on the bottom of the page without you having to manually calculate the distance.
However if you want to use the CSS, you can try:
.bottomdrawer {
position: fixed;
right: 0;
left: 0;
bottom: calc(env(safe-area-inset-bottom)-71vh);
...
}
.bottomdrawer[data-open="true"] {
bottom: calc(env(safe-area-inset-bottom)-79vh);

Related

Applying CSS classes to a dynamic collection of React nodes on a consistent delay

I have a dynamically sized collection of objects being passed into a Nav component that are being mapped and rendered as buttons. I want to apply a CSS animation to each button so that they slide in from off screen one at a time when the Nav component mounts. I figured that I would set up a loop through each one that updates a boolean value inside of a corresponding state object which applies the CSS class to the button to animate it, but each time that state object is updated, all of the buttons rerender which in turn starts all of the animations over. How can I prevent these rerenders?
// Nav.jsx
import React, { useState, useEffect } from 'react';
import { Button } from '../../../components';
import './Nav.scss';
const Nav = ({ actions }) => {
const [renderStates, setRenderStates] = useState(actions.reduce((accum, val) => {
return {...accum, [val.id]: false};
}, {}));
useEffect(() => {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const updateStates = async () => {
for (let i = 0; i < actions.length; i++) {
if (i > 0) {
await delay(75);
}
setRenderStates((prev) => ({
...prev,
[i]: true,
}));
};
};
updateStates();
}, [actions.length]);
return (
<div className='Nav'>
{actions.map((act) => (
<div className={`Nav__Button ${renderStates[act.id] ? 'Animate' : ''}`} key={act.id}>
<Button icon={act.icon} onClick={act.onClick} />
</div>
))}
</div>
);
};
export default Nav;
/* Nav.scss */
.Nav {
height: 100%;
width: fit-content;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
align-self: center;
padding: 1rem;
}
.Nav > * {
margin: 20% 0,
}
.Nav__Button {
margin-left: -5rem;
}
.Animate {
animation: slideInFromLeft .4s ease;
}
#keyframes slideInFromLeft {
0% {
margin-left: -5rem;
}
75% {
margin-left: .5rem;
}
100% {
margin-left: 0;
}
}
Here's a codesandbox that illustrates the problem (refresh the embedded browser to see the issue):
https://codesandbox.io/s/react-css-animations-on-timer-8mxnsz
Any help would be appreciated. Thanks.
You will need to create a from the elements inside actions.map and render a memoized version of it so that if the props do not change it will not re-render.
import { useState, useEffect, memo } from "react";
import "./styles.css";
const Test = ({ animate, label }) => {
return (
<div className={`Nav__Button ${animate ? "Animate" : ""}`}>
<button>{label}</button>
</div>
);
};
const TestMemo = memo(Test);
export default function App() {
const actions = [
{
id: 0,
label: "button 0"
},
{
id: 1,
label: "button 1"
},
{
id: 2,
label: "button 2"
},
{
id: 3,
label: "button 3"
}
];
const [renderStates, setRenderStates] = useState(
actions.reduce((accum, val) => {
return { ...accum, [val.id]: false };
}, {})
);
useEffect(() => {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const updateStates = async () => {
for (let i = 0; i < actions.length; i++) {
if (i > 0) {
await delay(2000);
}
setRenderStates((prev) => ({
...prev,
[i]: true
}));
}
};
updateStates();
}, [actions.length]);
return (
<div className="App">
{actions.map((act) => (
<TestMemo animate={renderStates[act.id]} label={act.label} />
))}
</div>
);
}

React component with state triggers animation in subcomponent when the state of parent component is changed

ProjectsSection component renders a document section with header and cards. A header animation should be triggered when the header is visible on the screen for the 1st time and run only once. However when I added a state to the ProjectsSection component with every state change a header animation runs and what is surprising to me only a part of it (letters/ spans that are children to h2 move, but brackets/ pseudoelements on h2 don't). In my project I use css modules for styling.
I have tried to wrap SectionHeader component in React.memo but it does not help.
Main component:
const ProjectsSection = () => {
const [openProjectCardId, setOpenProjectCardId] = useState('');
return (
<section id="projects" className="section">
<div className="container">
<SectionHeader>Projects</SectionHeader>
<div className={styles.content_wrapper}>
{projects.map((project, ind) => (
<Project
key={project.id}
project={project}
ind={ind}
isOpen={openProjectCardId === project.id}
setOpenProjectCardId={setOpenProjectCardId}
/>
))}
</div>
</div>
</section>
);
};
SectionHeader component:
const SectionHeader = ({ children }) => {
const headerRef = useIntersection(
styles.sectionHeader__isVisible,
{ rootMargin: '0px 0px -100px 0px', threshold: 1 },
);
const textToLetters = children.split('').map((letter) => {
const style = !letter ? styles.space : styles.letter;
return (
<span className={style} key={nanoid()}>
{letter}
</span>
);
});
return (
<div className={styles.sectionHeader_wrapper} ref={headerRef}>
<h2 className={styles.sectionHeader}>{textToLetters}</h2>
</div>
);
};
Css
.sectionHeader_wrapper {
position: relative;
// other properties
&::before {
display: block;
content: ' ';
position: absolute;
opacity: 0;
// other properties
}
&::after {
display: block;
content: ' ';
position: absolute;
opacity: 0;
// other properties
}
}
.sectionHeader {
position: relative;
display: inline-block;
overflow: hidden;
// other properties
}
.letter {
position: relative;
display: inline-block;
transform: translateY(100%) skew(0deg, 20deg);
}
.sectionHeader__isVisible .letter {
animation: typeletter .25s ease-out forwards;
}
useIntersection hook
const useIntersection = (
activeClass,
{ root = null, rootMargin = '0px', threshold = 1 },
dependency = [],
unobserveAfterFirstIntersection = true
) => {
const elementRef = useRef(null);
useEffect(() => {
const options: IntersectionObserverInit = {
root,
rootMargin,
threshold,
};
const observer = new IntersectionObserver((entries, observerObj) => {
entries.forEach((entry) => {
if (unobserveAfterFirstIntersection) {
if (entry.isIntersecting) {
entry.target.classList.add(activeClass);
observerObj.unobserve(entry.target);
}
} else if (entry.isIntersecting) {
entry.target.classList.add(activeClass);
} else {
entry.target.classList.remove(activeClass);
}
});
}, options);
// if (!elementRef.current) return;
if (elementRef.current) {
observer.observe(elementRef.current);
}
}, [...dependency]);
return elementRef;
};
I have found a solution to this problem.
This was apparently about textToLetters variable created every time the state of parent component was changing.
I used useMemo to keep this value and now animation is not triggered anymore.
const textToLetters = (text) =>
text.split('').map((letter) => {
const style = !letter ? styles.space : styles.letter;
return (
<span className={style} key={nanoid()}>
{letter}
</span>
);
});
const SectionHeader= ({ children }) => {
const headerRef = useIntersection(
styles.sectionHeader__isVisible,
{ rootMargin: '0px 0px -100px 0px', threshold: 1 },
[children]
);
const letters = React.useMemo(() => textToLetters(children), [children]);
return (
<div className={styles.sectionHeader_wrapper} ref={headerRef}>
<h2 className={styles.sectionHeader}>{letters}</h2>
</div>
);
};

how to make reactstrap carousel as shorter in size with left menu options?

I'm using reactstrap carousel in an application, but found problem when trying to make it shorter in size and a left ul menu with react.
import React from 'react';
import Shelf from '../Shelf';
import Filter from '../Shelf/Filter';
import GithubCorner from '../github/Corner';
import FloatCart from '../FloatCart';
import ControlledCarousel from './ControlledCarousel';
const App = () => (
<React.Fragment>
<GithubCorner />
<main>
<ControlledCarousel />
<Filter />
<Shelf />
</main>
<FloatCart />
</React.Fragment>
);
export default App;
--===================================================
import React, { Component } from 'react';
import {
Carousel,
CarouselItem,
CarouselControl,
CarouselIndicators,
CarouselCaption
} from 'reactstrap';
import './style.scss';
const items = [
{
src: 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa1d%20text%20%7B%20fill%3A%23555%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa1d%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23777%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22285.921875%22%20y%3D%22218.3%22%3EFirst%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E',
altText: 'Slide 1',
caption: 'Slide 1'
},
{
src: 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa20%20text%20%7B%20fill%3A%23444%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa20%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23666%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22247.3203125%22%20y%3D%22218.3%22%3ESecond%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E',
altText: 'Slide 2',
caption: 'Slide 2'
},
{
src: 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa21%20text%20%7B%20fill%3A%23333%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa21%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23555%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22277%22%20y%3D%22218.3%22%3EThird%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E',
altText: 'Slide 3',
caption: 'Slide 3'
}
];
class ControlledCarousel extends Component {
constructor(props) {
super(props);
this.state = { activeIndex: 0 };
this.next = this.next.bind(this);
this.previous = this.previous.bind(this);
this.goToIndex = this.goToIndex.bind(this);
this.onExiting = this.onExiting.bind(this);
this.onExited = this.onExited.bind(this);
}
onExiting() {
this.animating = true;
}
onExited() {
this.animating = false;
}
next() {
if (this.animating) return;
const nextIndex = this.state.activeIndex === items.length - 1 ? 0 : this.state.activeIndex + 1;
this.setState({ activeIndex: nextIndex });
}
previous() {
if (this.animating) return;
const nextIndex = this.state.activeIndex === 0 ? items.length - 1 : this.state.activeIndex - 1;
this.setState({ activeIndex: nextIndex });
}
goToIndex(newIndex) {
if (this.animating) return;
this.setState({ activeIndex: newIndex });
}
render() {
const { activeIndex } = this.state;
const slides = items.map((item) => {
return (
<CarouselItem className="slider-cart"
onExiting={this.onExiting}
onExited={this.onExited}
key={item.src}
>
<img src={item.src} alt={item.altText} />
<CarouselCaption captionText={item.caption} captionHeader={item.caption} />
</CarouselItem>
);
});
return (
<Carousel className="slider-cart"
activeIndex={activeIndex}
next={this.next}
previous={this.previous} >
<CarouselIndicators items={items} activeIndex={activeIndex} onClickHandler={this.goToIndex} />
{slides}
<CarouselControl direction="prev" directionText="Previous" onClickHandler={this.previous} />
<CarouselControl direction="next" directionText="Next" onClickHandler={this.next} />
</Carousel>
);
}
}
export default ControlledCarousel;
--===========================================
.carousel-inner > .item > img {
margin: 0 auto;
}
.slider-cart {
position: relative;
top: 0;
left:10px;
width: 450px;
height: 100%;
background-color: #1b1a20;
box-sizing: border-box;
transition: right 0.2s;
}
.carousel {
width: 900px;
height: 500px;
margin: auto;
}
#media (max-width: 900px) {
.carousel {
width: auto;
height: auto;
}
}
The code reports a problem and I have no idea with it since I'm a newbie with react-redux. What should I do in my code?
I've grabbed the example code for a carousel from here: https://reactstrap.github.io/components/carousel/
Application built with 16.9.0", "react-dom": "^16.9.0", "react-redux": "^7.1.0", "reactstrap": "^8.0.1","redux": "^4.0.4" and "redux-thunk": "^2.3.0".
The container uses the react custom component, which is disturbing the the layout.
Output:
I tried to follow guides and looked up example implementations but could not solve the issue.
reactjs reactstrap

Having trouble getting rid of the blue highlight

I've been working on a section with expandable/collapsible sections. When I click on a section to expand or collapse it, a blue focus area shows up but it is placed on a weird angle. I don't know what is causing it and would like a solution to either get rid of it or place it back at the normal horizontal angle. Does anybody have any suggestions as to how to fix this?
I am using a Macbook and Chrome browser.
The entire grey block that this component appears in is placed at an angle as you can see from the top of the image attached below but in the reverse direction from the highlighted focus area.
My css:
#import '../../theme/variables.css';
.rotatedSection {
padding-bottom: 2rem;
}
.container {
max-width: 64rem;
margin: 0 auto;
display: flex;
padding: 2rem 0;
#media screen and (max-width: 68rem) {
margin: 0 3rem;
}
}
.accordianContainer {
flex: 1;
margin-right: 2rem;
min-width: 500px;
#media screen and (max-width: $tablet-lg-max-width) {
margin-right: 0;
}
#media screen and (max-width: 900px) {
min-width: 0;
}
}
.imageContainer {
flex: 1;
margin-left: 2rem;
max-height: 300px;
display: flex;
justify-content: center;
img {
flex: 1;
}
#media screen and (max-width: $tablet-lg-max-width) {
margin-left: 0;
}
}
.heading {
composes: h2 from 'theme/text';
margin-left: auto;
margin-right: auto;
}
My react code:
import React, {Component, PropTypes} from 'react';
import RotatedSection from 'components/RotatedSection';
import AccordionItem from './AccordionItem';
import css from './styles.css';
class AccordionSectionWithImage extends Component {
constructor (props) {
super(props);
this.state = {
activeIndex: null,
};
this.onOpen = this.onOpen.bind(this);
this.onClose = this.onClose.bind(this);
this.setActive = this.setActive.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
}
onOpen = (index) => {
this.setActive(index);
};
onClose = (callback = () => null) => {
this.setActive(null);
callback();
};
setActive = (activeIndex) => this.setState({activeIndex});
handleClickOutside = () => this.props.collapseOnBlur && this.onClose();
render () {
const {
entry: {
items,
heading,
image,
},
showIndex,
classNames,
meta = {},
} = this.props;
const {routeParams, toggleHamburger} = meta;
const {activeIndex} = this.state;
return (
<RotatedSection color='whisper' className={css.rotatedSection}>
<div className={css.container}>
<div className={css.accordianContainer}>
<h2 className={css.heading}>{heading}</h2>
{items && items.map((item, index) => (
<AccordionItem
key={index}
showIndex={showIndex}
entry={item}
meta={{
position: index,
isOpen: (index === activeIndex),
onOpen: () => this.onOpen(index),
onClose: () => this.onClose(),
onChildClick: () => this.onClose(toggleHamburger),
routeParams,
}}
classNames={classNames}
/>
))}
</div>
<div className={css.imageContainer}>
<img src={image && image.fields && image.fields.file.url} alt='Educational assessment' />
</div>
</div>
</RotatedSection>
);
}
}
AccordionSectionWithImage.propTypes = {
meta: PropTypes.object,
entry: PropTypes.object,
collapseOnBlur: PropTypes.bool,
showIndex: PropTypes.bool,
classNames: PropTypes.object,
};
export default AccordionSectionWithImage;
React component for individual section:
function AccordionItem (props) {
const {
meta: {
isOpen,
onOpen,
onClose,
},
entry: {
heading,
text,
},
} = props;
const handleClick = () => (isOpen ? onClose() : onOpen());
return (
<div className={css.itemContainer}>
<div className={css.innerContainer}>
<h3 className={css.heading} onClick={handleClick}>
<span className={css.titleText}>{heading}</span>
<i className={`zmdi zmdi-plus ${css.titleToggle}`} />
</h3>
{isOpen && (
<div className={css.contents}>
{text}
</div>
)}
</div>
</div>
);
}
For anybody else experiencing a similar problem:
Problem only appeared on mobile phones and the device mode of chrome inspector. It was due to the tap-highlight property.
Setting -webkit-tap-highlight-color to rgba(0,0,0,0) hid the problem but it's a non standard css property so the solution may not work for all devices/browsers/users.

How do I render D3 with D3-force in Meteor-React setting?

I'm trying to place this behind my login box. The svg element is rendering but none of the contents. The rest of the login screen as well as the rest of the website works as expected. Even when I comment out all of the other html and JSX code, the svg doesn't render the particles within it. I even have another D3 element (just a graph) rendering just fine, so I suspect that there's an issue with D3-force. I have the latest d3 and d3-force installed from npm.
Here's all the relevant CSS:
* {
margin: 0;
padding: 0;
}
html {
font-size: 62.5%;
}
body {
font-family: Helvetica, Arial, sans-serif;
font-size: $base-font-size;
//background-color: $grey;
}
.boxed-view {
align-items: center;
background: $boxed-view-overlay-bg;
display: flex;
justify-content: center;
height: 100vh;
width: 100vw;
}
.boxed-view__box {
background-color: $boxed-view-bg;
margin-bottom: $space;
padding: 2.4rem;
text-align: center;
width: 28rem;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
overflow: scroll;
}
And here's all the relevant JS:
import d3 from 'd3';
import React from 'react';
import d3_force from 'd3-force';
import {Session} from 'meteor/session';
import {Tracker} from 'meteor/tracker';
import {Bodies} from '../api/bodies';
export default class BrownianSplash extends React.Component {
constructor(props) {
super(props);
this.state = {
bodies: [],
isOpen_add: false,
isOpen_view: false,
error: ''
};
}
onSubmit(e) {
let username = this.refs.username.value.trim();
let password = this.refs.password.value.trim();
e.preventDefault(); // prevent page refresh
Meteor.loginWithPassword({username}, password, (err) => {
if (err) {
console.log('Login callback', err);
this.setState({error: err.reason});
} else {
this.setState({error: ''});
}
});
}
componentDidMount() {
this.bodyNamesTracker = Tracker.autorun(() => {
Meteor.subscribe('bodies_names');
const bodies = Bodies.find({}).fetch();
this.setState({bodies});
});
}
componentWillUnmount() {
this.bodyNamesTracker.stop(); // we don't want to set the state every time the page is loaded
}
renderAnimation() {
const INIT_DENSITY = 0.00025, // particles per sq px
PARTICLE_RADIUS_RANGE = [1, 12],
PARTICLE_VELOCITY_RANGE = [0, 4];
const canvasWidth = window.innerWidth,
canvasHeight = window.innerHeight,
svgCanvas = d3.select('svg#canvas')
.attr('width', canvasWidth)
.attr('height', canvasHeight);
const forceSim = d3_force.forceSimulation()
.alphaDecay(0)
.velocityDecay(0)
.on('tick', particleDigest)
.force('bounce', d3_force.forceBounce()
.radius(d => d.r)
)
.force('container', d3_force.forceSurface()
.surfaces([
{from: {x:0,y:0}, to: {x:0,y:canvasHeight}},
{from: {x:0,y:canvasHeight}, to: {x:canvasWidth,y:canvasHeight}},
{from: {x:canvasWidth,y:canvasHeight}, to: {x:canvasWidth,y:0}},
{from: {x:canvasWidth,y:0}, to: {x:0,y:0}}
])
.oneWay(true)
.radius(d => d.r)
);
// Init particles
onDensityChange(INIT_DENSITY);
// Event handlers
function onDensityChange(density) {
const newNodes = genNodes(density);
// d3.select('#numparticles-val').text(newNodes.length);
// d3.select('#density-control').attr('defaultValue', density);
forceSim.nodes(newNodes);
}
function onElasticityChange(elasticity) {
// d3.select('#elasticity-val').text(elasticity);
// forceSim.force('bounce').elasticity(elasticity);
// forceSim.force('container').elasticity(elasticity);
}
//
function genNodes(density) {
const numParticles = Math.round(canvasWidth * canvasHeight * density),
existingParticles = forceSim.nodes();
// Trim
if (numParticles < existingParticles.length) {
return existingParticles.slice(0, numParticles);
}
// Append
return [...existingParticles, ...d3_force.range(numParticles - existingParticles.length).map(() => {
const angle = Math.random() * 2 * Math.PI,
velocity = Math.random() * (PARTICLE_VELOCITY_RANGE[1] - PARTICLE_VELOCITY_RANGE[0]) + PARTICLE_VELOCITY_RANGE[0];
return {
x: Math.random() * canvasWidth,
y: Math.random() * canvasHeight,
vx: Math.cos(angle) * velocity,
vy: Math.sin(angle) * velocity,
r: Math.round(Math.random() * (PARTICLE_RADIUS_RANGE[1] - PARTICLE_RADIUS_RANGE[0]) + PARTICLE_RADIUS_RANGE[0])
}
})];
}
function particleDigest() {
let particle = svgCanvas.selectAll('circle.particle').data(forceSim.nodes().map(hardLimit));
particle.exit().remove();
particle.merge(
particle.enter().append('circle')
.classed('particle', true)
.attr('r', d=>d.r)
.attr('fill', 'darkslategrey')
)
.attr('cx', d => d.x)
.attr('cy', d => d.y);
}
function hardLimit(node) {
// Keep in canvas
node.x = Math.max(node.r, Math.min(canvasWidth-node.r, node.x));
node.y = Math.max(node.r, Math.min(canvasHeight-node.r, node.y));
return node;
}
}
render() {
return (
<div>
<svg id="canvas"></svg>
<div id="controls"></div>
<div className='boxed-view'>
{/* D3 background goes here */}
{/* <svg className='boxed-view'></svg> */}
<div className='boxed-view__box'>
<h1>Login</h1>
{this.state.error ? <p>{this.state.error}</p> : undefined}
<form onSubmit={this.onSubmit.bind(this)} className='boxed-view__form'>
<input type="text" name='username' placeholder="Josiah Carberry" ref='username'/>
<input type="password" name='password' ref='password'/>
<button className='button'>Let's Go</button>
</form>
</div>
</div>
</div>
);
}
}
So I didn't know that d3-force-bounce and d3-force-surface existed. They're both npm packages and are default exports. The methods d3.forceBounce() and d3.forceSurface() are methods of d3ForceBounce and d3ForceSurface, not d3. Finally, I forgot to call renderAnimation() in the JSX.

Resources