-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Add the Component Library to the app #8319
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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']; | ||
Comment on lines
+15
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if there is a way for you to construct this array of values based on the styles of the buttons defined in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sadly, it is a limitation from typescript. We can get types from runtime objects, but we cannot get runtime objects from types 😅 Nevertheless, it is something we can consider for the future. |
||
|
||
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; |
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]; | ||
}; |
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: how come at MM, we don't name the file like we would the component?
ButtonComponentLibrary.tsx vs button.cl.tsx?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general we do. In this case I am making up a new standard just because 😅 The idea here is that since it is the
component library
for thebutton
component, then...button.cl.tsx
. Similar for when you have the tests for the button component it would bebutton.test.tsx
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As an example, our animated_number/index.tsx, would have been
animated_number/AnimatedNumber.tsx
and the test would have been in the same folder by the name ofAnimatedNumber.test.tsx
. The component name isAnimatedNumber
. If I have so manyindex.tsx
file open on tabs of vscode, it's just more confusing.So, if you have a file called ButtonComponentLibrary.tsx, and your component name is ButtonComponentLibrary, then it matches and much easier to open the file.
Just wondering, not trying to get things changed here.
Visual representation of current and files named by component.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So... that is an interesting thing we can deal with. One of the problems is that you want to import components as:
path/to/my/component
instead ofpath/to/my/component/component
. If you don't have an index file, that is what happens.In many components, we use the index file to get the state from the database (similar in webapp, where we use it to get the state from redux). But some components don't have a need for that. You may argue that then, we can remove the folder, but having the folder helps organizing other files that may be related.
A solution that we can do is having "empty index files". With some of the migrations I have done in web towards react hooks, we have decided to follow that path. We have an index file that is practically just two lines:
We can follow that approach here too, but migrating to that approach may be time consuming (and break a bit the blame history).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I'm more of a path/to/my/component/Component.tsx kinda developer/guy. 😺
So you would have
/src/components/animated_number/AnimatedNumber.tsx
/src/components/animated_number/AnimatedNumber.test.tsx
instead of
/src/components/animated_number/index.tsx
Kinda like react-native Component...
packages/react-native/Libraries/Components/TextInput/TextInput.js
(again, not advocating a change, just curious more than anything else, spawning from having 5-10 index.tsx tabs on my vscode)