Skip to content

Commit

Permalink
Add the Component Library to the app (#8319)
Browse files Browse the repository at this point in the history
* Add the Component Library to the app

* fix i18n

---------

Co-authored-by: Elias Nahum <[email protected]>
  • Loading branch information
larkox and enahum committed Feb 4, 2025
1 parent 89ad44c commit 926fdf9
Show file tree
Hide file tree
Showing 10 changed files with 454 additions and 2 deletions.
2 changes: 2 additions & 0 deletions app/constants/screens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const CONVERT_GM_TO_CHANNEL = 'ConvertGMToChannel';
export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage';
export const CREATE_OR_EDIT_CHANNEL = 'CreateOrEditChannel';
export const CREATE_TEAM = 'CreateTeam';
export const COMPONENT_LIBRARY = 'ComponentLibrary';
export const CUSTOM_STATUS = 'CustomStatus';
export const CUSTOM_STATUS_CLEAR_AFTER = 'CustomStatusClearAfter';
export const DRAFT = 'Draft';
Expand Down Expand Up @@ -98,6 +99,7 @@ export default {
CHANNEL_NOTIFICATION_PREFERENCES,
CODE,
CONVERT_GM_TO_CHANNEL,
COMPONENT_LIBRARY,
CREATE_DIRECT_MESSAGE,
CREATE_OR_EDIT_CHANNEL,
CREATE_TEAM,
Expand Down
4 changes: 4 additions & 0 deletions app/context/theme/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ const ThemeProvider = ({currentTeamId, children, themes}: Props) => {
return (<Provider value={theme}>{children}</Provider>);
};

export const CustomThemeProvider = ({theme, children}: {theme: Theme; children: React.ReactNode}) => {
return (<Provider value={theme}>{children}</Provider>);
};

export function withTheme<T extends WithThemeProps>(Component: ComponentType<T>): ComponentType<T> {
return function ThemeComponent(props) {
return (
Expand Down
65 changes: 65 additions & 0 deletions app/screens/component_library/button.cl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React, {useMemo} from 'react';
import {Alert, View} from 'react-native';

import Button from '@components/button';
import {useTheme} from '@context/theme';

import {useBooleanProp, useDropdownProp, useStringProp} from './hooks';
import {buildComponent} from './utils';

const propPossibilities = {};

const buttonSizeValues = ['xs', 's', 'm', 'lg'];
const buttonEmphasisValues = ['primary', 'secondary', 'tertiary', 'link'];
const buttonTypeValues = ['default', 'destructive', 'inverted', 'disabled'];
const buttonStateValues = ['default', 'hover', 'active', 'focus'];

const onPress = () => Alert.alert('Button pressed!');

const ButtonComponentLibrary = () => {
const theme = useTheme();
const [text, textSelector] = useStringProp('text', 'Some text', false);
const [disabled, disabledSelector] = useBooleanProp('disabled', false);
const [buttonSize, buttonSizePosibilities, buttonSizeSelector] = useDropdownProp('size', 'm', buttonSizeValues, true);
const [buttonEmphasis, buttonEmphasisPosibilities, buttonEmphasisSelector] = useDropdownProp('emphasis', 'primary', buttonEmphasisValues, true);
const [buttonType, buttonTypePosibilities, buttonTypeSelector] = useDropdownProp('buttonType', 'default', buttonTypeValues, true);
const [buttonState, buttonStatePosibilities, buttonStateSelector] = useDropdownProp('buttonState', 'default', buttonStateValues, true);

const components = useMemo(
() => buildComponent(Button, propPossibilities, [
buttonSizePosibilities,
buttonEmphasisPosibilities,
buttonTypePosibilities,
buttonStatePosibilities,
], [
text,
disabled,
buttonSize,
buttonEmphasis,
buttonType,
buttonState,
{
theme,
onPress,
},
]),
[buttonEmphasis, buttonEmphasisPosibilities, buttonSize, buttonSizePosibilities, buttonState, buttonStatePosibilities, buttonType, buttonTypePosibilities, disabled, text, theme],
);

return (
<>
{textSelector}
{disabledSelector}
{buttonSizeSelector}
{buttonEmphasisSelector}
{buttonTypeSelector}
{buttonStateSelector}
<View>{components}</View>
</>
);
};

export default ButtonComponentLibrary;
107 changes: 107 additions & 0 deletions app/screens/component_library/hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React, {useCallback, useMemo, useState} from 'react';

import AutocompleteSelector from '@components/autocomplete_selector';
import BoolSetting from '@components/settings/bool_setting';
import TextSetting from '@components/settings/text_setting';

type HookResult<T> = [
{[x: string]: T},
JSX.Element,
]
export const useStringProp = (
propName: string,
defaultValue: string,
isTextarea: boolean,
): HookResult<string> => {
const [value, setValue] = useState(defaultValue);
const selector = useMemo(() => (
<TextSetting
label={propName}
multiline={isTextarea}
disabled={false}
keyboardType='default'
onChange={setValue}
optional={false}
secureTextEntry={false}
testID={`${propName}.input`}
value={value}
/>
), [value, propName, isTextarea]);
const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]);

return [preparedProp, selector];
};

export const useBooleanProp = (
propName: string,
defaultValue: boolean,
): HookResult<boolean> => {
const [value, setValue] = useState(defaultValue);
const selector = useMemo(() => (
<BoolSetting
onChange={setValue}
testID={`${propName}.input`}
value={value}
label={propName}
/>
), [propName, value]);
const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]);

return [preparedProp, selector];
};

const ALL_OPTION = 'ALL';
type DropdownHookResult = [
{[x: string]: string} | undefined,
{[x: string]: string[]} | undefined,
JSX.Element,
];
export const useDropdownProp = (
propName: string,
defaultValue: string,
options: string[],
allowAll: boolean,
): DropdownHookResult => {
const [value, setValue] = useState(defaultValue);
const onChange = useCallback((v: SelectedDialogOption) => {
if (!v) {
setValue(defaultValue);
return;
}
if (Array.isArray(v)) {
setValue(v[0].value);
return;
}

setValue(v.value);
}, [defaultValue]);

const renderedOptions = useMemo(() => {
const toReturn: DialogOption[] = options.map((v) => ({
value: v,
text: v,
}));
if (allowAll) {
toReturn.unshift({
value: ALL_OPTION,
text: ALL_OPTION,
});
}
return toReturn;
}, [options, allowAll]);
const selector = useMemo(() => (
<AutocompleteSelector
testID={`${propName}.input`}
label={propName}
onSelected={onChange}
options={renderedOptions}
selected={value}
/>
), [onChange, propName, renderedOptions, value]);
const preparedProp = useMemo(() => (value === ALL_OPTION ? undefined : ({[propName]: value})), [propName, value]);
const preparedPossibilities = useMemo(() => (value === ALL_OPTION ? ({[propName]: options}) : undefined), [propName, value, options]);
return [preparedProp, preparedPossibilities, selector];
};
145 changes: 145 additions & 0 deletions app/screens/component_library/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React, {useCallback, useMemo, useState} from 'react';
import {ScrollView, View, type StyleProp, type ViewStyle} from 'react-native';

import AutocompleteSelector from '@components/autocomplete_selector';
import {Preferences} from '@constants';
import {CustomThemeProvider} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {popTopScreen} from '@screens/navigation';

import ButtonComponentLibrary from './button.cl';

import type {AvailableScreens} from '@typings/screens/navigation';

const componentMap = {
Button: ButtonComponentLibrary,
};

type ComponentName = keyof typeof componentMap
const defaultComponent = Object.keys(componentMap)[0] as ComponentName;
const componentOptions = Object.keys(componentMap).map((v) => ({
value: v,
text: v,
}));

type ThemeName = keyof typeof Preferences.THEMES;
const defaultTheme = Object.keys(Preferences.THEMES)[0] as ThemeName;
const themeOptions = Object.keys(Preferences.THEMES).map((v) => ({
value: v,
text: v,
}));

type BackgroundType = 'center' | 'sidebar';
const backgroundOptions = [{
value: 'center',
text: 'Center channel',
}, {
value: 'sidebar',
text: 'Sidebar background',
}];

type Props = {
componentId: AvailableScreens;
};

const ComponentLibrary = ({componentId}: Props) => {
const [selectedComponent, setSelectedComponent] = useState<ComponentName>(defaultComponent);
const onSelectComponent = useCallback((value: SelectedDialogOption) => {
if (!value) {
setSelectedComponent(defaultComponent);
return;
}

if (Array.isArray(value)) {
setSelectedComponent(value[0].value as ComponentName);
return;
}
setSelectedComponent(value.value as ComponentName);
}, []);

const [selectedTheme, setSelectedTheme] = useState(defaultTheme);
const onSelectTheme = useCallback((value: SelectedDialogOption) => {
if (!value) {
setSelectedTheme(defaultTheme);
return;
}

if (Array.isArray(value)) {
setSelectedTheme(value[0].value as ThemeName);
return;
}
setSelectedTheme(value.value as ThemeName);
}, []);

const [selectedBackground, setSelectedBackground] = useState<BackgroundType>('center');
const onSelectBackground = useCallback((value: SelectedDialogOption) => {
if (!value) {
setSelectedBackground('center');
return;
}

if (Array.isArray(value)) {
setSelectedBackground(value[0].value as BackgroundType);
return;
}
setSelectedBackground(value.value as BackgroundType);
}, []);

const backgroundStyle: StyleProp<ViewStyle> = useMemo(() => {
const theme = Preferences.THEMES[selectedTheme];
switch (selectedBackground) {
case 'center':
return {
backgroundColor: theme.centerChannelBg,
};
case 'sidebar':
default:
return {
backgroundColor: theme.sidebarBg,
};
}
}, [selectedBackground, selectedTheme]);

const close = useCallback(() => {
popTopScreen(componentId);
}, [componentId]);

useAndroidHardwareBackHandler(componentId, close);

const SelectedComponent = componentMap[selectedComponent];
return (
<ScrollView style={{margin: 10}}>
<AutocompleteSelector
testID='selectedComponent'
label='Component'
onSelected={onSelectComponent}
selected={selectedComponent}
options={componentOptions}
/>
<AutocompleteSelector
testID='selectedTheme'
label='Theme'
onSelected={onSelectTheme}
selected={selectedTheme}
options={themeOptions}
/>
<AutocompleteSelector
testID='selectedBackground'
label='Background'
onSelected={onSelectBackground}
selected={selectedBackground}
options={backgroundOptions}
/>
<View style={backgroundStyle}>
<CustomThemeProvider theme={Preferences.THEMES[selectedTheme]}>
<SelectedComponent/>
</CustomThemeProvider>
</View>
</ScrollView>
);
};

export default ComponentLibrary;
Loading

0 comments on commit 926fdf9

Please sign in to comment.