Skip to content

Commit

Permalink
[C] make widgets controlled
Browse files Browse the repository at this point in the history
  • Loading branch information
alexgoff committed Nov 8, 2023
1 parent c7d9492 commit 34d815b
Show file tree
Hide file tree
Showing 14 changed files with 287 additions and 291 deletions.
4 changes: 2 additions & 2 deletions packages/epo-widget-lib/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rubin-epo/epo-widget-lib",
"version": "0.4.3",
"version": "0.5.0",
"description": "Rubin Observatory Education & Public Outreach team React scientific and educational widgets.",
"author": "Rubin EPO",
"license": "MIT",
Expand Down Expand Up @@ -86,7 +86,7 @@
"react-i18next": "^12.2.0"
},
"dependencies": {
"@rubin-epo/epo-react-lib": "^2.0.0",
"@rubin-epo/epo-react-lib": "^2.0.1",
"lodash": "^4.17.21",
"styled-components": "^6.0.4",
"use-resize-observer": "^9.1.0"
Expand Down
41 changes: 14 additions & 27 deletions packages/epo-widget-lib/src/atomic/Blinker/Blinker.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { FunctionComponent, useEffect, useState } from "react";
import { FunctionComponent, PropsWithChildren, useState } from "react";
import { ImageShape } from "@rubin-epo/epo-react-lib";
import { tokens } from "@rubin-epo/epo-react-lib/styles";
import useInterval from "@/hooks/useInterval";
import useResizeObserver from "use-resize-observer";
import * as Styled from "./styles";
import { getClampedArrayIndex } from "@/lib/utils";

export interface BlinkerProps {
images: ImageShape[];
activeIndex?: number;
activeIndex: number;
autoplay?: boolean;
loop?: boolean;
interval?: number;
Expand All @@ -17,33 +15,20 @@ export interface BlinkerProps {
className?: string;
}

const Blinker: FunctionComponent<BlinkerProps> = ({
const Blinker: FunctionComponent<PropsWithChildren<BlinkerProps>> = ({
images = [],
activeIndex: presetIndex = 0,
activeIndex = 0,
autoplay = true,
loop = true,
interval = 200,
blinkCallback,
loadedCallback,
className,
children,
}) => {
const { ref, width = 1 } = useResizeObserver<HTMLDivElement>();
const [playing, setPlaying] = useState(autoplay);
const [activeIndex, setActiveIndex] = useState(presetIndex);
const [loaded, setLoaded] = useState(false);
const { BREAK_MOBILE } = tokens;
const canBlink = images.length > 1;
const isCondensed = width < parseInt(BREAK_MOBILE);

useEffect(() => {
blinkCallback && blinkCallback(activeIndex);
}, [activeIndex]);

useEffect(() => {
if (loaded) {
loadedCallback && loadedCallback();
}
}, [loaded]);

const getBlink = (direction = 0) => {
const lastIndex = images.length - 1;
Expand All @@ -58,8 +43,7 @@ const Blinker: FunctionComponent<BlinkerProps> = ({
if (loop === false && nextIndex === images.length - 1) {
stopBlink();
}

setActiveIndex(nextIndex);
blinkCallback && blinkCallback(nextIndex);
}
};

Expand All @@ -76,17 +60,21 @@ const Blinker: FunctionComponent<BlinkerProps> = ({
};
const handlePrevious = () => {
stopBlink();
setActiveIndex(getBlink(-1));
blinkCallback && blinkCallback(getBlink(-1));
};

useInterval(nextBlink, canBlink && loaded && playing ? interval : null);

return (
<Styled.BlinkerContainer className={className} ref={ref}>
<Styled.BlinkerContainer className={className}>
<Styled.BlinkerImages
loadedCallback={() => setLoaded(true)}
{...{ images, activeIndex, $isCondensed: isCondensed }}
loadedCallback={() => {
setLoaded(true);
loadedCallback && loadedCallback();
}}
{...{ images, activeIndex }}
/>
{children}
{canBlink && (
<Styled.BlinkerControls
isDisabled={!loaded}
Expand All @@ -95,7 +83,6 @@ const Blinker: FunctionComponent<BlinkerProps> = ({
handleStartStop,
handleNext,
handlePrevious,
$isCondensed: isCondensed,
}}
/>
)}
Expand Down
4 changes: 2 additions & 2 deletions packages/epo-widget-lib/src/atomic/Blinker/Image/Image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ const BlinkerImage: FunctionComponent<ImageProps> = ({
<Styled.BlinkerImage
alt={altText}
src={url}
onLoad={loadCallback}
$active={active}
onLoad={() => loadCallback && loadCallback()}
style={{ "--image-visibility": active ? "visible" : "hidden" }}
/>
);
};
Expand Down
14 changes: 3 additions & 11 deletions packages/epo-widget-lib/src/atomic/Blinker/Image/styles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import styled, { css } from "styled-components";
import styled from "styled-components";

export const BlinkerImage = styled.img<{ $active: boolean }>`
export const BlinkerImage = styled.img`
position: absolute;
top: 0;
right: 0;
Expand All @@ -15,13 +15,5 @@ export const BlinkerImage = styled.img<{ $active: boolean }>`
-o-user-drag: none;
-ms-user-drag: none;
user-drag: none;
${({ $active }) =>
$active
? css`
visibility: visible;
`
: css`
visibility: hidden;
`}
visibility: var(--image-visibility, hidden);
`;
19 changes: 10 additions & 9 deletions packages/epo-widget-lib/src/atomic/Blinker/Images/Images.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FunctionComponent, useEffect, useState } from "react";
import { FunctionComponent, useCallback, useEffect, useState } from "react";
import CircularLoader from "@rubin-epo/epo-react-lib/CircularLoader";
import * as Styled from "./styles";
import BlinkerImage from "../Image/Image";
Expand All @@ -17,24 +17,25 @@ const Images: FunctionComponent<ImagesProps> = ({
loadedCallback,
}) => {
const [imagesLoaded, setImagesLoaded] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const isLoading = imagesLoaded !== images.length;

const loadCallback = () => {
setImagesLoaded((count) => count + 1);
};
const loadCallback = useCallback(
() => setImagesLoaded((count) => count + 1),
[]
);

useEffect(() => {
setIsLoading(imagesLoaded !== images.length);

if (!isLoading) {
loadedCallback && loadedCallback();
}
}, [imagesLoaded, isLoading]);
}, [isLoading]);

return (
<Styled.BlinkContainer data-testid="blinker-images" className={className}>
{!imagesLoaded && <CircularLoader isVisible={isLoading} />}
<Styled.LoadingContainer $isLoading={isLoading}>
<Styled.LoadingContainer
style={{ "--loading-opacity": isLoading ? 0 : 1 }}
>
{images.map((image, i) => {
const { url } = image;
const active = activeIndex === i;
Expand Down
14 changes: 3 additions & 11 deletions packages/epo-widget-lib/src/atomic/Blinker/Images/styles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import styled, { css } from "styled-components";
import styled from "styled-components";

export const BlinkContainer = styled.div`
background-color: var(--neutral95, #1f2121);
Expand All @@ -8,15 +8,7 @@ export const BlinkContainer = styled.div`
position: relative;
`;

export const LoadingContainer = styled.div<{ $isLoading: boolean }>`
export const LoadingContainer = styled.div`
opacity: var(--loading-opacity, 0);
transition: opacity ease var(--DURATION_SLOW, 0.4s);
${({ $isLoading }) =>
$isLoading
? css`
opacity: 0;
`
: css`
opacity: 1;
`}
`;
36 changes: 16 additions & 20 deletions packages/epo-widget-lib/src/atomic/Blinker/styles.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import styled, { css } from "styled-components";
import styled from "styled-components";
import { token } from "@rubin-epo/epo-react-lib/styles";
import Controls from "./Controls/Controls";
import Images from "./Images/Images";

export const BlinkerContainer = styled.div`
container: blinker / inline-size;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr min-content;
Expand All @@ -11,30 +13,24 @@ export const BlinkerContainer = styled.div`
height: 100%;
`;

export const BlinkerControls = styled(Controls)<{ $isCondensed: boolean }>`
const breakSize: string = token("BREAK_MOBILE") as string;

export const BlinkerControls = styled(Controls)`
grid-row: 2;
margin-block-start: var(--PADDING_SMALL, 20px);
${({ $isCondensed }) =>
$isCondensed
? css`
margin-block-start: var(--PADDING_SMALL, 20px);
`
: css`
margin-block-end: var(--PADDING_SMALL, 20px);
`}
@container blinker (min-width: ${breakSize}) {
margin-block-end: var(--PADDING_SMALL, 20px);
}
`;

export const BlinkerImages = styled(Images)<{ $isCondensed: boolean }>`
export const BlinkerImages = styled(Images)`
aspect-ratio: 1;
grid-row: 1;
width: 100%;
${({ $isCondensed }) =>
$isCondensed
? css`
grid-row: 1;
`
: css`
position: absolute;
grid-row: span 2;
`};
@container blinker (min-width: ${breakSize}) {
position: absolute;
grid-row: span 2;
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ const CameraFilter: FunctionComponent = () => {
</colgroup>
<Styled.FilterNames>
<tr>
{filters.map(({ band }) => (
{filters.map(({ band }, i) => (
<Styled.FilterName
id={`${band}-name`}
key={band}
key={i}
band={band}
scope="col"
aria-hidden={!band}
Expand Down
62 changes: 39 additions & 23 deletions packages/epo-widget-lib/src/widgets/ColorTool/ColorTool.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { ComponentMeta, ComponentStoryObj } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import {
singleData,
multiData,
colorOptions,
multiSpectralOptions,
readOnlyData,
} from "./__mocks__";
import { prepareData } from "./utilities";

import ColorTool from ".";
import { Option } from "@rubin-epo/epo-react-lib";
import { useState } from "react";

const meta: ComponentMeta<typeof ColorTool> = {
const meta: Meta<typeof ColorTool> = {
argTypes: {
isDisplayOnly: {
control: "boolean",
Expand Down Expand Up @@ -144,12 +146,28 @@ const meta: ComponentMeta<typeof ColorTool> = {
};
export default meta;

export const Primary: ComponentStoryObj<typeof ColorTool> = {
args: {
data: singleData,
selectedData: singleData[0].objects[0],
colorOptions,
},
const Template: StoryFn<typeof ColorTool> = (args) => {
const [selectedData, setSelectedData] = useState(
prepareData(args.selectedData)
);

return (
<ColorTool
{...args}
selectedData={selectedData}
selectionCallback={(selected) => {
setSelectedData(selected);
args.selectionCallback && args.selectionCallback(selected);
}}
/>
);
};

export const Primary: StoryFn<typeof ColorTool> = Template.bind({});
Primary.args = {
data: singleData,
selectedData: singleData[0].objects[0],
colorOptions,
};

const objectOptions: Option[] = [];
Expand All @@ -164,21 +182,19 @@ multiData.forEach((category) => {
});
});

export const MultipleImages: ComponentStoryObj<typeof ColorTool> = {
args: {
data: multiData,
objectOptions,
selectedData: multiData[0].objects[0],
colorOptions: multiSpectralOptions,
},
export const MultipleImages: StoryFn<typeof ColorTool> = Template.bind({});
MultipleImages.args = {
data: multiData,
objectOptions,
selectedData: multiData[0].objects[0],
colorOptions: multiSpectralOptions,
};

export const DisplayOnly: ComponentStoryObj<typeof ColorTool> = {
args: {
data: readOnlyData,
objectOptions,
selectedData: readOnlyData[0].objects[0],
colorOptions: multiSpectralOptions,
isDisplayOnly: true,
},
export const DisplayOnly: StoryFn<typeof ColorTool> = Template.bind({});
DisplayOnly.args = {
data: readOnlyData,
objectOptions,
selectedData: readOnlyData[0].objects[0],
colorOptions: multiSpectralOptions,
isDisplayOnly: true,
};
Loading

0 comments on commit 34d815b

Please sign in to comment.