mirror of https://github.com/jitsi/jitsi-meet
feat(ui-components) Add Select component (#12182)
Remove @atlaskit/dropdown Convert some files to TSpull/12184/head jitsi-meet_7784
parent
f5e60a7ca4
commit
2d6e181a13
Before Width: | Height: | Size: 494 B After Width: | Height: | Size: 482 B |
@ -0,0 +1,179 @@ |
||||
import { Theme } from '@mui/material'; |
||||
import React, { ChangeEvent } from 'react'; |
||||
import { makeStyles } from 'tss-react/mui'; |
||||
|
||||
import { isMobileBrowser } from '../../../environment/utils'; |
||||
import Icon from '../../../icons/components/Icon'; |
||||
import { IconArrowDown } from '../../../icons/svg'; |
||||
import { withPixelLineHeight } from '../../../styles/functions.web'; |
||||
|
||||
interface SelectProps { |
||||
|
||||
/** |
||||
* Helper text to be displayed below the select. |
||||
*/ |
||||
bottomLabel?: string; |
||||
|
||||
/** |
||||
* Class name for additional styles. |
||||
*/ |
||||
className?: string; |
||||
|
||||
/** |
||||
* Wether or not the select is disabled. |
||||
*/ |
||||
disabled?: boolean; |
||||
|
||||
/** |
||||
* Wether or not the select is in the error state. |
||||
*/ |
||||
error?: boolean; |
||||
|
||||
/** |
||||
* Label to be displayed above the select. |
||||
*/ |
||||
label?: string; |
||||
|
||||
/** |
||||
* Change handler. |
||||
*/ |
||||
onChange: (e: ChangeEvent<HTMLSelectElement>) => void; |
||||
|
||||
/** |
||||
* The options of the select. |
||||
*/ |
||||
options: Array<{ |
||||
label: string; |
||||
value: number | string; |
||||
}>; |
||||
|
||||
/** |
||||
* The value of the select. |
||||
*/ |
||||
value: number | string; |
||||
} |
||||
|
||||
const useStyles = makeStyles()((theme: Theme) => { |
||||
return { |
||||
container: { |
||||
display: 'flex', |
||||
flexDirection: 'column' |
||||
}, |
||||
|
||||
label: { |
||||
color: theme.palette.text01, |
||||
...withPixelLineHeight(theme.typography.bodyShortRegular), |
||||
marginBottom: theme.spacing(2), |
||||
|
||||
'&.is-mobile': { |
||||
...withPixelLineHeight(theme.typography.bodyShortRegularLarge) |
||||
} |
||||
}, |
||||
|
||||
selectContainer: { |
||||
position: 'relative' |
||||
}, |
||||
|
||||
select: { |
||||
backgroundColor: theme.palette.ui03, |
||||
borderRadius: `${theme.shape.borderRadius}px`, |
||||
width: '100%', |
||||
...withPixelLineHeight(theme.typography.bodyShortRegular), |
||||
color: theme.palette.text01, |
||||
padding: '8px 16px', |
||||
paddingRight: '42px', |
||||
border: 0, |
||||
appearance: 'none', |
||||
overflow: 'hidden', |
||||
whiteSpace: 'nowrap', |
||||
textOverflow: 'ellipsis', |
||||
|
||||
'&:focus': { |
||||
outline: 0, |
||||
boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}` |
||||
}, |
||||
|
||||
'&:disabled': { |
||||
color: theme.palette.text03 |
||||
}, |
||||
|
||||
'&.is-mobile': { |
||||
...withPixelLineHeight(theme.typography.bodyShortRegularLarge), |
||||
padding: '12px 16px', |
||||
paddingRight: '46px' |
||||
}, |
||||
|
||||
'&.error': { |
||||
boxShadow: `0px 0px 0px 2px ${theme.palette.textError}` |
||||
} |
||||
}, |
||||
|
||||
icon: { |
||||
position: 'absolute', |
||||
top: '8px', |
||||
right: '8px', |
||||
pointerEvents: 'none', |
||||
|
||||
'&.is-mobile': { |
||||
top: '12px', |
||||
right: '12px' |
||||
} |
||||
}, |
||||
|
||||
bottomLabel: { |
||||
marginTop: theme.spacing(2), |
||||
...withPixelLineHeight(theme.typography.labelRegular), |
||||
color: theme.palette.text02, |
||||
|
||||
'&.is-mobile': { |
||||
...withPixelLineHeight(theme.typography.bodyShortRegular) |
||||
}, |
||||
|
||||
'&.error': { |
||||
color: theme.palette.textError |
||||
} |
||||
} |
||||
}; |
||||
}); |
||||
|
||||
const Select = ({ |
||||
bottomLabel, |
||||
className, |
||||
disabled, |
||||
error, |
||||
label, |
||||
onChange, |
||||
options, |
||||
value }: SelectProps) => { |
||||
const { classes, cx, theme } = useStyles(); |
||||
const isMobile = isMobileBrowser(); |
||||
|
||||
return ( |
||||
<div className = { classes.container }> |
||||
{label && <span className = { cx(classes.label, isMobile && 'is-mobile') }>{label}</span>} |
||||
<div className = { classes.selectContainer }> |
||||
<select |
||||
className = { cx(classes.select, isMobile && 'is-mobile', className, error && 'error') } |
||||
disabled = { disabled } |
||||
onChange = { onChange } |
||||
value = { value }> |
||||
{options.map(option => (<option |
||||
key = { option.value } |
||||
value = { option.value }>{option.label}</option>))} |
||||
</select> |
||||
<Icon |
||||
className = { cx(classes.icon, isMobile && 'is-mobile') } |
||||
color = { disabled ? theme.palette.icon03 : theme.palette.icon01 } |
||||
size = { 22 } |
||||
src = { IconArrowDown } /> |
||||
</div> |
||||
{bottomLabel && ( |
||||
<span className = { cx(classes.bottomLabel, isMobile && 'is-mobile', error && 'error') }> |
||||
{bottomLabel} |
||||
</span> |
||||
)} |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export default Select; |
@ -1,196 +0,0 @@ |
||||
/* @flow */ |
||||
|
||||
import { |
||||
DropdownItem, |
||||
DropdownItemGroup, |
||||
DropdownMenuStateless |
||||
} from '@atlaskit/dropdown-menu'; |
||||
import React, { PureComponent } from 'react'; |
||||
|
||||
import { translate } from '../../../../base/i18n'; |
||||
import { YOUTUBE_LIVE_DASHBOARD_URL } from '../constants'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link StreamKeyPicker}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* Broadcasts available for selection. Each broadcast item should be an |
||||
* object with a title for display in the dropdown and a boundStreamID to |
||||
* return in the {@link onBroadcastSelected} callback. |
||||
*/ |
||||
broadcasts: Array<Object>, |
||||
|
||||
/** |
||||
* Callback invoked when an item in the dropdown is selected. The selected |
||||
* broadcast's boundStreamID will be passed back. |
||||
*/ |
||||
onBroadcastSelected: Function, |
||||
|
||||
/** |
||||
* The boundStreamID of the broadcast that should display as selected in the |
||||
* dropdown. |
||||
*/ |
||||
selectedBoundStreamID: string, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} state of {@link StreamKeyPicker}. |
||||
*/ |
||||
type State = { |
||||
|
||||
/** |
||||
* Whether or not to display the dropdown menu to pick a YouTube broadcast. |
||||
*/ |
||||
isDropdownOpen: boolean |
||||
}; |
||||
|
||||
/** |
||||
* A dropdown to select a YouTube broadcast. |
||||
* |
||||
* @augments Component |
||||
*/ |
||||
class StreamKeyPicker extends PureComponent<Props, State> { |
||||
/** |
||||
* Default values for {@code StreamKeyForm} component's properties. |
||||
* |
||||
* @static |
||||
*/ |
||||
static defaultProps = { |
||||
broadcasts: [] |
||||
}; |
||||
|
||||
/** |
||||
* The initial state of a {@code StreamKeyForm} instance. |
||||
*/ |
||||
state = { |
||||
isDropdownOpen: false |
||||
}; |
||||
|
||||
/** |
||||
* Initializes a new {@code StreamKeyPicker} instance. |
||||
* |
||||
* @param {Props} props - The React {@code Component} props to initialize |
||||
* the new {@code StreamKeyPicker} instance with. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onDropdownOpenChange = this._onDropdownOpenChange.bind(this); |
||||
this._onSelect = this._onSelect.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { broadcasts, selectedBoundStreamID, t } = this.props; |
||||
|
||||
if (!broadcasts.length) { |
||||
return ( |
||||
<a |
||||
className = 'warning-text' |
||||
href = { YOUTUBE_LIVE_DASHBOARD_URL } |
||||
rel = 'noopener noreferrer' |
||||
target = '_blank'> |
||||
{ t('liveStreaming.getStreamKeyManually') } |
||||
</a> |
||||
); |
||||
} |
||||
|
||||
const dropdownItems |
||||
= broadcasts.map(broadcast => ( |
||||
<DropdownItem |
||||
data-streamid = { broadcast.boundStreamID } |
||||
key = { broadcast.boundStreamID } |
||||
onClick = { this._onSelect }> |
||||
{ broadcast.title } |
||||
</DropdownItem>)); |
||||
const selected |
||||
= this.props.broadcasts.find( |
||||
broadcast => broadcast.boundStreamID === selectedBoundStreamID); |
||||
const triggerText |
||||
= (selected && selected.title) || t('liveStreaming.choose'); |
||||
|
||||
return ( |
||||
<div className = 'broadcast-dropdown dropdown-menu'> |
||||
<DropdownMenuStateless |
||||
isOpen = { this.state.isDropdownOpen } |
||||
onItemActivated = { this._onSelect } |
||||
onOpenChange = { this._onDropdownOpenChange } |
||||
shouldFitContainer = { true } |
||||
trigger = { triggerText } |
||||
triggerButtonProps = {{ |
||||
className: 'broadcast-dropdown-trigger', |
||||
shouldFitContainer: true |
||||
}} |
||||
triggerType = 'button'> |
||||
<DropdownItemGroup> |
||||
{ dropdownItems } |
||||
</DropdownItemGroup> |
||||
</DropdownMenuStateless> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Transforms the passed in broadcasts into an array of objects that can |
||||
* be parsed by {@code DropdownMenuStateless}. |
||||
* |
||||
* @param {Array<Object>} broadcasts - The YouTube broadcasts to display. |
||||
* @private |
||||
* @returns {Array<Object>} |
||||
*/ |
||||
_formatBroadcasts(broadcasts) { |
||||
return broadcasts.map(broadcast => { |
||||
return { |
||||
content: broadcast.title, |
||||
value: broadcast |
||||
}; |
||||
}); |
||||
} |
||||
|
||||
_onDropdownOpenChange: (Object) => void; |
||||
|
||||
/** |
||||
* Sets the dropdown to be displayed or not based on the passed in event. |
||||
* |
||||
* @param {Object} dropdownEvent - The event passed from |
||||
* {@code DropdownMenuStateless} indicating if the dropdown should be open |
||||
* or closed. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onDropdownOpenChange(dropdownEvent) { |
||||
this.setState({ |
||||
isDropdownOpen: dropdownEvent.isOpen |
||||
}); |
||||
} |
||||
|
||||
_onSelect: (string) => void; |
||||
|
||||
/** |
||||
* Callback invoked when an item has been clicked in the dropdown menu. |
||||
* |
||||
* @param {Object} e - The key event to handle. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_onSelect(e) { |
||||
const streamId = e.currentTarget.getAttribute('data-streamid'); |
||||
|
||||
this.props.onBroadcastSelected(streamId); |
||||
} |
||||
} |
||||
|
||||
export default translate(StreamKeyPicker); |
@ -0,0 +1,125 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { WithTranslation } from 'react-i18next'; |
||||
|
||||
import { translate } from '../../../../base/i18n/functions'; |
||||
import Select from '../../../../base/ui/components/web/Select'; |
||||
import { YOUTUBE_LIVE_DASHBOARD_URL } from '../constants'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link StreamKeyPicker}. |
||||
*/ |
||||
interface Props extends WithTranslation { |
||||
|
||||
/** |
||||
* Broadcasts available for selection. Each broadcast item should be an |
||||
* object with a title for display in the dropdown and a boundStreamID to |
||||
* return in the {@link onBroadcastSelected} callback. |
||||
*/ |
||||
broadcasts: Array<{ |
||||
boundStreamID: string; |
||||
title: string; |
||||
}>; |
||||
|
||||
/** |
||||
* Callback invoked when an item in the dropdown is selected. The selected |
||||
* broadcast's boundStreamID will be passed back. |
||||
*/ |
||||
onBroadcastSelected: Function; |
||||
|
||||
/** |
||||
* The boundStreamID of the broadcast that should display as selected in the |
||||
* dropdown. |
||||
*/ |
||||
selectedBoundStreamID: string; |
||||
} |
||||
|
||||
/** |
||||
* A dropdown to select a YouTube broadcast. |
||||
* |
||||
* @augments Component |
||||
*/ |
||||
class StreamKeyPicker extends PureComponent<Props> { |
||||
/** |
||||
* Default values for {@code StreamKeyForm} component's properties. |
||||
* |
||||
* @static |
||||
*/ |
||||
static defaultProps = { |
||||
broadcasts: [] |
||||
}; |
||||
|
||||
/** |
||||
* The initial state of a {@code StreamKeyForm} instance. |
||||
*/ |
||||
state = { |
||||
isDropdownOpen: false |
||||
}; |
||||
|
||||
/** |
||||
* Initializes a new {@code StreamKeyPicker} instance. |
||||
* |
||||
* @param {Props} props - The React {@code Component} props to initialize |
||||
* the new {@code StreamKeyPicker} instance with. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onSelect = this._onSelect.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { broadcasts, selectedBoundStreamID, t } = this.props; |
||||
|
||||
if (!broadcasts.length) { |
||||
return ( |
||||
<a |
||||
className = 'warning-text' |
||||
href = { YOUTUBE_LIVE_DASHBOARD_URL } |
||||
rel = 'noopener noreferrer' |
||||
target = '_blank'> |
||||
{ t('liveStreaming.getStreamKeyManually') } |
||||
</a> |
||||
); |
||||
} |
||||
|
||||
const dropdownItems |
||||
= broadcasts.map(broadcast => { |
||||
return { |
||||
value: broadcast.boundStreamID, |
||||
label: broadcast.title |
||||
}; |
||||
}); |
||||
|
||||
return ( |
||||
<div className = 'broadcast-dropdown dropdown-menu'> |
||||
<Select |
||||
label = { t('liveStreaming.choose') } |
||||
onChange = { this._onSelect } |
||||
options = { dropdownItems } |
||||
value = { selectedBoundStreamID } /> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Callback invoked when an item has been clicked in the dropdown menu. |
||||
* |
||||
* @param {Object} e - The key event to handle. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_onSelect(e: React.ChangeEvent<HTMLSelectElement>) { |
||||
const streamId = e.target.value; |
||||
|
||||
this.props.onBroadcastSelected(streamId); |
||||
} |
||||
} |
||||
|
||||
export default translate(StreamKeyPicker); |
Loading…
Reference in new issue