Skip to content

Commit

Permalink
Merge pull request #22 from deriv-com/shafin/GRWT-4929/feat-duration-…
Browse files Browse the repository at this point in the history
…input

Shafin/grwt 4929/feat duration input
  • Loading branch information
shafin-deriv authored Feb 6, 2025
2 parents 2502be9 + 52d9cac commit a6a8ba7
Show file tree
Hide file tree
Showing 41 changed files with 2,168 additions and 228 deletions.
14 changes: 13 additions & 1 deletion STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,21 @@ components/
├── BottomNav/ # Navigation component
├── BottomSheet/ # Modal sheet component
├── Chart/ # Price chart
├── DurationOptions/ # Trade duration
├── Duration/ # Trade duration selection
│ ├── components/ # Duration subcomponents
│ │ ├── DurationTab.tsx
│ │ ├── DurationTabList.tsx
│ │ ├── DurationValueList.tsx
│ │ └── HoursDurationValue.tsx
│ └── DurationController.tsx
├── DurationOptions/ # Legacy trade duration (to be deprecated)
├── TradeButton/ # Trade execution
├── TradeFields/ # Trade parameters
└── ui/ # Shared UI components
├── button.tsx
├── card.tsx
├── chip.tsx # Selection chip component
├── primary-button.tsx # Primary action button
├── switch.tsx
└── toggle.tsx
```
Expand Down Expand Up @@ -214,6 +223,9 @@ RSBUILD_SSE_PROTECTED_PATH=/sse
- Use composition over inheritance
- Keep components focused and single-responsibility
- Document props and side effects
- Implement reusable UI components in ui/ directory
- Use TailwindCSS for consistent styling
- Support theme customization through design tokens

3. **State Management**
- Use local state for UI-only state
Expand Down
30 changes: 30 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,36 @@ The application is structured around modular, self-contained components and uses
- [Configuration Files](STRUCTURE.md#configuration-files): Build, development, and testing configurations
- [Module Dependencies](STRUCTURE.md#module-dependencies): Core and development dependencies

## Typography System

The application uses a consistent typography system based on IBM Plex Sans:

### Text Styles

#### Caption Regular (Small Text)
```css
font-family: IBM Plex Sans;
font-size: 12px;
font-weight: 400;
line-height: 18px;
text-align: left;
```

#### Body Regular (Default Text)
```css
font-family: IBM Plex Sans;
font-size: 16px;
font-weight: 400;
line-height: 24px;
text-align: left;
```

### Layout Principles
- Components should take full available width where appropriate
- Text content should be consistently left-aligned
- Maintain proper spacing and padding for readability
- Use responsive design patterns for different screen sizes

## Architecture

- [Component Structure](src/components/README.md): Comprehensive guide on TDD implementation, Atomic Component Design, and component organization
Expand Down
6 changes: 3 additions & 3 deletions src/components/BottomSheet/BottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const BottomSheet = () => {
<>
{/* Overlay */}
<div
className="fixed inset-0 bg-black/80 z-50 animate-in fade-in-0"
className="fixed inset-0 bg-black/80 z-[60] animate-in fade-in-0"
onClick={() => {
// Only close if clicking the overlay itself, not its children
onDragDown?.();
Expand All @@ -92,7 +92,7 @@ export const BottomSheet = () => {
rounded-t-[16px]
animate-in fade-in-0 slide-in-from-bottom
duration-300
z-50
z-[60]
transition-transform
overflow-hidden
`}
Expand All @@ -106,7 +106,7 @@ export const BottomSheet = () => {
</div>

{/* Content */}
<div className="flex-1 overflow-y-auto p-4">{body}</div>
<div className="flex-1 overflow-y-auto">{body}</div>
</div>
</>
);
Expand Down
43 changes: 35 additions & 8 deletions src/components/BottomSheet/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# BottomSheet Component

## Overview
A reusable bottom sheet component with drag-to-dismiss functionality and drag callback support.
A reusable bottom sheet component that provides a mobile-friendly interface with smooth animations, drag-to-dismiss functionality, and theme-aware styling.

## Features
- Single instance pattern using Zustand store
- Dynamic height support (%, px, vh)
- Theme-aware using Tailwind CSS variables
- Smooth animations for enter/exit transitions
- Drag gesture support with callback
- Content management through configuration
- Responsive overlay with fade effect

## Usage

Expand Down Expand Up @@ -68,27 +70,52 @@ interface BottomSheetState {
- Proper cleanup on sheet close and unmount

## Styling
Uses Tailwind CSS variables for theme support:
Uses Tailwind CSS for theme-aware styling and animations:
```tsx
// Theme colors
className="bg-background" // Theme background
className="bg-muted" // Theme muted color
className="bg-black/80" // Semi-transparent overlay

// Animations
className="animate-in fade-in-0" // Fade in animation
className="slide-in-from-bottom" // Slide up animation
className="duration-300" // Animation duration
className="transition-transform" // Smooth transform transitions

// Layout
className="rounded-t-[16px]" // Rounded top corners
className="max-w-[800px]" // Maximum width
className="overflow-hidden" // Content overflow handling
```

## Implementation Details

### Touch Event Handling
```typescript
const handleTouchMove = (e: TouchEvent) => {
if (!isDragging.current) return;
const handleTouchMove = useCallback((e: TouchEvent) => {
if (!sheetRef.current || !isDragging.current) return;

const touch = e.touches[0];
const deltaY = touch.clientY - dragStartY.current;
currentY.current = deltaY;

const deltaY = e.touches[0].clientY - dragStartY.current;
if (deltaY > 0) {
// Update sheet position
sheetRef.current.style.transform = `translateY(${deltaY}px)`;
// Call drag callback if provided
onDragDown?.();
}
};
}, [onDragDown]);
```

### Overlay Handling
```tsx
<div
className="fixed inset-0 bg-black/80 z-[60] animate-in fade-in-0"
onClick={() => {
onDragDown?.();
setBottomSheet(false);
}}
/>
```

### Height Processing
Expand Down
43 changes: 42 additions & 1 deletion src/components/BottomSheet/__tests__/BottomSheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,13 @@ describe("BottomSheet", () => {
expect(bottomSheet).toHaveStyle({ height: '50vh' });
});

it("handles drag to dismiss", () => {
it("handles drag to dismiss and calls onDragDown", () => {
const mockOnDragDown = jest.fn();
mockUseBottomSheetStore.mockReturnValue({
showBottomSheet: true,
key: 'test-key',
height: '380px',
onDragDown: mockOnDragDown,
setBottomSheet: mockSetBottomSheet
});

Expand All @@ -83,6 +85,45 @@ describe("BottomSheet", () => {
fireEvent.touchMove(document, { touches: [{ clientY: 150 }] });
fireEvent.touchEnd(document);

expect(mockOnDragDown).toHaveBeenCalled();
expect(mockSetBottomSheet).toHaveBeenCalledWith(false);
});

it("applies animation classes when shown", () => {
mockUseBottomSheetStore.mockReturnValue({
showBottomSheet: true,
key: 'test-key',
height: '380px',
setBottomSheet: mockSetBottomSheet
});

const { container } = render(<BottomSheet />);

const overlay = container.querySelector('[class*="fixed inset-0"]');
const sheet = container.querySelector('[class*="fixed bottom-0"]');

expect(overlay?.className).toContain('animate-in fade-in-0');
expect(sheet?.className).toContain('animate-in fade-in-0 slide-in-from-bottom');
expect(sheet?.className).toContain('duration-300');
});

it("calls onDragDown when clicking overlay", () => {
const mockOnDragDown = jest.fn();
mockUseBottomSheetStore.mockReturnValue({
showBottomSheet: true,
key: 'test-key',
height: '380px',
onDragDown: mockOnDragDown,
setBottomSheet: mockSetBottomSheet
});

const { container } = render(<BottomSheet />);

const overlay = container.querySelector('[class*="fixed inset-0"]');
expect(overlay).toBeInTheDocument();
fireEvent.click(overlay!);

expect(mockOnDragDown).toHaveBeenCalled();
expect(mockSetBottomSheet).toHaveBeenCalledWith(false);
});

Expand Down
96 changes: 96 additions & 0 deletions src/components/Duration/DurationController.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from "react";
import { DurationTabList } from "./components/DurationTabList";
import { DurationValueList } from "./components/DurationValueList";
import { HoursDurationValue } from "./components/HoursDurationValue";
import { useTradeStore } from "@/stores/tradeStore";
import { useBottomSheetStore } from "@/stores/bottomSheetStore";
import { PrimaryButton } from "@/components/ui/primary-button";

const getDurationValues = (type: string): number[] => {
switch (type) {
case "tick":
return [1, 2, 3, 4, 5];
case "second":
return [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
57, 58, 59, 60,
];
case "minute":
return [1, 2, 3, 5, 10, 15, 30];
case "hour":
return [1, 2, 3, 4, 6, 8, 12, 24];
case "day":
return [1];
default:
return [];
}
};

export const DurationController: React.FC = () => {
const { duration, setDuration } = useTradeStore();
const { setBottomSheet } = useBottomSheetStore();

// Initialize local state with store value
const [localDuration, setLocalDuration] = React.useState(duration);
const [value, type] = localDuration.split(" ");
const selectedType = type;
const selectedValue: string | number = type === "hour" ? value : parseInt(value, 10);

const handleTypeSelect = (type: string) => {
if (type === "hour") {
setLocalDuration("1:0 hour");
} else {
const values = getDurationValues(type);
const newValue = values[0];
setLocalDuration(`${newValue} ${type}`);
}
};

const handleValueSelect = (value: number) => {
setLocalDuration(`${value} ${selectedType}`);
};

const handleSave = () => {
setDuration(localDuration); // Update store with local state
setBottomSheet(false);
};

return (
<div className="flex flex-col h-full" id="DurationController">
<div>
<h5 className="font-ubuntu text-[16px] font-bold leading-[24px] text-center underline decoration-transparent py-4 px-2">
Duration
</h5>
<DurationTabList
selectedType={selectedType}
onTypeSelect={handleTypeSelect}
/>
</div>
<div className="flex-1 relative px-8">
<div
className="absolute inset-x-0 top-1/2 -translate-y-1/2 h-[48px] pointer-events-none"
id="duration-selection-zone"
/>
{selectedType === "hour" ? (
<HoursDurationValue
selectedValue={selectedValue.toString()}
onValueSelect={(value) => setLocalDuration(`${value} hour`)}
/>
) : (
<DurationValueList
key={selectedType}
selectedValue={selectedValue as number}
durationType={selectedType}
onValueSelect={handleValueSelect}
getDurationValues={getDurationValues}
/>
)}
</div>
<div className="w-full p-3">
<PrimaryButton onClick={handleSave}>Save</PrimaryButton>
</div>
</div>
);
};
Loading

0 comments on commit a6a8ba7

Please sign in to comment.