Skip to content

feat: ✨ confirm dialog component #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions components/Dialogs/Confirm/HorizontalButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';

import { Colors, Text, TouchableOpacity, View } from 'react-native-ui-lib';

import { useUpdate } from './context';

export interface ConfirmDialogHorizontalButtonsProps {
/**
* The function to call when the confirm button is pressed
*/
onConfirm: () => void;

/**
* The text to display on the confirm button
* @default 'Yes'
*/
confirmText?: string;

/**
* The text to display on the cancel button
* @default 'No'
*/
cancelText?: string;

/**
* The color of the confirm button
* @default '$textSuccess'
*/
confirmColor?: keyof typeof Colors;

/**
* The color of the cancel button
* @default '$textDefault'
*/
cancelColor?: keyof typeof Colors;
}

export const HorizontalButtons = ({
onConfirm,
cancelText = 'No',
confirmText = 'Yes',
cancelColor = '$textDanger',
confirmColor = '$textSuccess',
}: ConfirmDialogHorizontalButtonsProps) => {
const update = useUpdate();

const handleClose = () => update({ visible: false });

const handleConfirm = () => {
handleClose();
onConfirm();
};

return (
<View row marginT-s10>
<TouchableOpacity onPress={handleClose} center style={{ width: '50%' }}>
<Text text80H {...{ [cancelColor]: true }}>
{cancelText}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={handleConfirm}
center
style={{
width: '50%',
borderLeftWidth: 1,
borderLeftColor: Colors.$backgroundNeutral,
}}
>
<Text text80H {...{ [confirmColor]: true }}>
{confirmText}
</Text>
</TouchableOpacity>
</View>
);
};
HorizontalButtons.displayName = 'ConfirmDialog.HorizontalButtons';
72 changes: 72 additions & 0 deletions components/Dialogs/Confirm/Root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';

import { Card, Dialog } from 'react-native-ui-lib';

import { useContext, useUpdate } from './context';

export interface ConfirmDialogProps {
/**
* The description text if title is not enough
*/
children?: React.ReactNode;
}

export type ConfirmDialogRef = {
/**
* Open the dialog
*/
open: () => void;
};

/**
* A dialog that asks the user to confirm an action
*
* @example
* ```tsx
* const ref = React.useRef<ConfirmDialogRef>(null);
*
* const handlePress = () => {
* ref.current?.open();
* };
*
* const handleDialogConfirm = () => {
* ref.current?.close();
* // do something
* };
*
* return (
* <>
* <ConfirmDialog ref={ref}>
* <ConfirmDialog.Title>Save Session</ConfirmDialog.Title>
* <Text>Are you sure you want to save this session?</Text>
* <ConfirmDialog.HorizontalButtons
* onConfirm={handleDialogConfirm}
* />
* </ConfirmDialog>
* <Button label="Save" onPress={handlePress} />
* </>
* );
* ```
*/
export const Root = React.forwardRef<ConfirmDialogRef, ConfirmDialogProps>(
({ children }, ref) => {
const { visible } = useContext();
const update = useUpdate();

const handleOpen = () => update({ visible: true });
const handleClose = () => update({ visible: false });

React.useImperativeHandle(ref, () => ({
open: handleOpen,
}));

return (
<Dialog visible={visible} onDismiss={handleClose}>
<Card padding-s4 gap-s4>
{children}
</Card>
</Dialog>
);
}
);
Root.displayName = 'ConfirmDialog.Root';
9 changes: 9 additions & 0 deletions components/Dialogs/Confirm/Title.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';

import { Text as RNText } from 'react-native';
import { Text, TextProps } from 'react-native-ui-lib';

export const Title = React.forwardRef<RNText, TextProps>((props, ref) => (
<Text ref={ref} text70M {...props} />
));
Title.displayName = 'ConfirmDialog.Title';
78 changes: 78 additions & 0 deletions components/Dialogs/Confirm/VerticalButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';

import { Button, Colors, View } from 'react-native-ui-lib';

import { useUpdate } from './context';

export interface ConfirmDialogVerticalButtonsProps {
/**
* The function to call when the confirm button is pressed
*/
onConfirm: () => void;

/**
* The text to display on the confirm button
* @default 'Yes'
*/
confirmText?: string;

/**
* The text to display on the cancel button
* @default 'No'
*/
cancelText?: string;

/**
* The color of the confirm button
* @default '$textSuccess'
*/
confirmColor?: keyof typeof Colors;

/**
* The color of the cancel button
* @default '$textDefault'
*/
cancelColor?: keyof typeof Colors;

/**
* If true, the buttons will be reversed
* @default false
*/
reversed?: boolean;
}

export const VerticalButtons = ({
onConfirm,
cancelText = 'No',
confirmText = 'Yes',
cancelColor = '$textDanger',
confirmColor = '$textSuccess',
reversed = false,
}: ConfirmDialogVerticalButtonsProps) => {
const update = useUpdate();

const handleClose = () => update({ visible: false });

const handleConfirm = () => {
handleClose();
onConfirm();
};

return (
<View gap-s4 marginT-s4>
<Button
label={reversed ? cancelText : confirmText}
onPress={reversed ? handleClose : handleConfirm}
backgroundColor={reversed ? Colors[cancelColor] : Colors[confirmColor]}
/>
<Button
label={reversed ? confirmText : cancelText}
onPress={reversed ? handleConfirm : handleClose}
size="small"
link
linkColor={reversed ? Colors[confirmColor] : Colors[cancelColor]}
/>
</View>
);
};
VerticalButtons.displayName = 'ConfirmDialog.VerticalButtons';
7 changes: 7 additions & 0 deletions components/Dialogs/Confirm/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { contextBuilder } from '@htk/utils/react/contextBuilder';

const [ConfirmDialogProvider, useContext, useDispatch, useUpdate] = contextBuilder({
visible: false,
});

export { ConfirmDialogProvider, useContext, useDispatch, useUpdate };
18 changes: 18 additions & 0 deletions components/Dialogs/Confirm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

import { ConfirmDialogProvider } from './context';
import { HorizontalButtons } from './HorizontalButtons';
import { ConfirmDialogProps, ConfirmDialogRef, Root } from './Root';
import { Title } from './Title';
import { VerticalButtons } from './VerticalButtons';

export { ConfirmDialogRef, ConfirmDialogProps };

export const ConfirmDialog = Object.assign(
React.forwardRef<ConfirmDialogRef, ConfirmDialogProps>((props, ref) => (
<ConfirmDialogProvider>
<Root ref={ref} {...props} />
</ConfirmDialogProvider>
)),
{ Title, HorizontalButtons, VerticalButtons }
);
1 change: 1 addition & 0 deletions components/Dialogs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Confirm';
118 changes: 118 additions & 0 deletions components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# HTK React Native Components

These components are reusable parts for all Expo/React Native projects.

## Required NPM Packages
- `react-native-ui-lib` - A versatile library that offers a variety of useful
components and utilities.

## Rules

The following rules must be adhered to:

### Folder Structure
Each component or component group should have its own separate folder.

**Reason**:
A component may be too large to reside in a single file and may need to be
split into multiple files to enhance performance and readability. The folder
name should reflect the component name. The folder may contain multiple
subcomponents that are private to the component itself.

:x: Bad:
```sh
./components/Button.tsx
```

:white_check_mark: Good:
```sh
./components/Button/index.tsx
```

### Filetype
All files must be in TypeScript to provide error checking and autocompletion.
The correct extension for TypeScript JSX files is .tsx.

### TypeScript Types
When using props in a component:
- Use `interface`
- The name must be `<ComponentName>Props`
- `export` the interface

**Reason**:
Interfaces are easier to extend. There might be a need to wrap the component
for special cases, utilizing the component's props.

:x: Bad
```tsx
// Bad because:
// - It does not use `interface`
// - The name is too generic
// - It is not exported
type Props = {
// ...
}

export function Button(props: Props) {
// ...
}
```

:white_check_mark: Good:
```tsx
export interface ButtonProps {
// ...
}

export function Button(props: Props) {
// ...
}
```

### Extensive docstrings
**Reason**:
To improve documentation and assist the team in easily utilizing the component,
each prop and the component itself must have docstrings.

:x: Bad
```tsx
export interface ButtonProps {
label: string;
onPress?: () => void;
}

export function Button(props: Props) {
// ...
}
```

:white_check_mark: Good:
```tsx
export interface ButtonProps {
/**
* Text to displayed inside the button.
*/
label: string;

/**
* Callback function that is called when the button is pressed.
*/
onPress?: () => void;
}

/**
* Button Component
*
* @example
* ```tsx
* const handlePress = () => {
* // ...
* };
* return <Button label="Foo" onPress={handlePress} />;
*
* ```
*/
export function Button(props: Props) {
// ...
}
```
Loading