mirror of https://github.com/jitsi/jitsi-meet
feat(welcome-page): Redesign. (#3559)
* feat(welcome-page): Redesign. * Style adjustments.pull/3565/head jitsi-meet_3385
parent
62b6737a3f
commit
b30008e3a5
@ -0,0 +1,117 @@ |
||||
.meetings-list { |
||||
font-size: 14px; |
||||
color: #253858; |
||||
line-height: 20px; |
||||
text-align: left; |
||||
text-overflow: ellipsis; |
||||
display: flex; |
||||
flex-direction: column; |
||||
position: relative; |
||||
width: 100%; |
||||
height: 100%; |
||||
overflow: auto; |
||||
|
||||
.meetings-list-empty { |
||||
text-align: center; |
||||
align-items: center; |
||||
justify-content: center; |
||||
display: flex; |
||||
flex-grow: 1; |
||||
flex-direction: column; |
||||
|
||||
.description { |
||||
font-size: 16px; |
||||
padding: 20px; |
||||
} |
||||
} |
||||
|
||||
.button { |
||||
background: #0074E0; |
||||
border-radius: 4px; |
||||
color: #FFFFFF; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
padding: 5px 10px; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.item { |
||||
background: rgba(255,255,255,0.50); |
||||
box-sizing: border-box; |
||||
display: inline-flex; |
||||
margin-top: 5px; |
||||
min-height: 92px; |
||||
width: 100%; |
||||
word-break: break-word; |
||||
display: flex; |
||||
flex-direction: row; |
||||
text-align: left; |
||||
|
||||
&:first-child { |
||||
margin-top: 0px; |
||||
} |
||||
|
||||
.left-column { |
||||
display: flex; |
||||
flex-direction: column; |
||||
width: 140px; |
||||
flex-grow: 0; |
||||
padding-left: 30px; |
||||
padding-top: 25px; |
||||
|
||||
.date { |
||||
font-weight: bold; |
||||
padding-bottom: 5px; |
||||
} |
||||
} |
||||
|
||||
.right-column { |
||||
display: flex; |
||||
flex-direction: column; |
||||
flex-grow: 1; |
||||
padding-left: 30px; |
||||
padding-top: 25px; |
||||
|
||||
.title { |
||||
font-size: 16px; |
||||
font-weight: bold; |
||||
padding-bottom: 5px; |
||||
} |
||||
} |
||||
|
||||
.actions { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
flex-grow: 0; |
||||
padding-right: 30px; |
||||
} |
||||
|
||||
&.with-click-handler { |
||||
cursor: pointer; |
||||
} |
||||
|
||||
&.with-click-handler:hover { |
||||
background-color: #75A7E7; |
||||
} |
||||
|
||||
.add-button { |
||||
width: 30px; |
||||
height: 30px; |
||||
padding: 0px; |
||||
} |
||||
|
||||
i { |
||||
cursor: inherit; |
||||
} |
||||
|
||||
.join-button { |
||||
display: none; |
||||
} |
||||
|
||||
&:hover .join-button { |
||||
display: block |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,201 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import { |
||||
getLocalizedDateFormatter, |
||||
getLocalizedDurationFormatter |
||||
} from '../../../i18n'; |
||||
|
||||
import Container from './Container'; |
||||
import Text from './Text'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Indicates if the list is disabled or not. |
||||
*/ |
||||
disabled: boolean, |
||||
|
||||
/** |
||||
* Indicates if the URL should be hidden or not. |
||||
*/ |
||||
hideURL: boolean, |
||||
|
||||
/** |
||||
* Function to be invoked when an item is pressed. The item's URL is passed. |
||||
*/ |
||||
onPress: Function, |
||||
|
||||
/** |
||||
* Rendered when the list is empty. Should be a rendered element. |
||||
*/ |
||||
listEmptyComponent: Object, |
||||
|
||||
/** |
||||
* An array of meetings. |
||||
*/ |
||||
meetings: Array<Object>, |
||||
|
||||
/** |
||||
* Defines what happens when an item in the section list is clicked |
||||
*/ |
||||
onItemClick: Function |
||||
}; |
||||
|
||||
/** |
||||
* Generates a date string for a given date. |
||||
* |
||||
* @param {Object} date - The date. |
||||
* @private |
||||
* @returns {string} |
||||
*/ |
||||
function _toDateString(date) { |
||||
return getLocalizedDateFormatter(date).format('MMM Do, YYYY'); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Generates a time (interval) string for a given times. |
||||
* |
||||
* @param {Array<Date>} times - Array of times. |
||||
* @private |
||||
* @returns {string} |
||||
*/ |
||||
function _toTimeString(times) { |
||||
if (times && times.length > 0) { |
||||
return ( |
||||
times |
||||
.map(time => getLocalizedDateFormatter(time).format('LT')) |
||||
.join(' - ')); |
||||
} |
||||
|
||||
return undefined; |
||||
} |
||||
|
||||
/** |
||||
* Implements a React/Web {@link Component} for displaying a list with |
||||
* meetings. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
export default class MeetingsList extends Component<Props> { |
||||
/** |
||||
* Constructor of the MeetingsList component. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this._onPress = this._onPress.bind(this); |
||||
this._renderItem = this._renderItem.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Renders the content of this component. |
||||
* |
||||
* @returns {React.ReactNode} |
||||
*/ |
||||
render() { |
||||
const { listEmptyComponent, meetings } = this.props; |
||||
|
||||
/** |
||||
* If there are no recent meetings we don't want to display anything |
||||
*/ |
||||
if (meetings) { |
||||
return ( |
||||
<Container |
||||
className = 'meetings-list'> |
||||
{ |
||||
meetings.length === 0 |
||||
? listEmptyComponent |
||||
: meetings.map(this._renderItem) |
||||
} |
||||
</Container> |
||||
); |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
_onPress: string => Function; |
||||
|
||||
/** |
||||
* Returns a function that is used in the onPress callback of the items. |
||||
* |
||||
* @param {string} url - The URL of the item to navigate to. |
||||
* @private |
||||
* @returns {Function} |
||||
*/ |
||||
_onPress(url) { |
||||
const { disabled, onPress } = this.props; |
||||
|
||||
if (!disabled && url && typeof onPress === 'function') { |
||||
return () => onPress(url); |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
_renderItem: (Object, number) => React$Node; |
||||
|
||||
/** |
||||
* Renders an item for the list. |
||||
* |
||||
* @param {Object} meeting - Information about the meeting. |
||||
* @param {number} index - The index of the item. |
||||
* @returns {Node} |
||||
*/ |
||||
_renderItem(meeting, index) { |
||||
const { |
||||
date, |
||||
duration, |
||||
elementAfter, |
||||
time, |
||||
title, |
||||
url |
||||
} = meeting; |
||||
const { hideURL = false } = this.props; |
||||
const onPress = this._onPress(url); |
||||
const rootClassName |
||||
= `item ${ |
||||
onPress ? 'with-click-handler' : 'without-click-handler'}`;
|
||||
|
||||
return ( |
||||
<Container |
||||
className = { rootClassName } |
||||
key = { index } |
||||
onClick = { onPress }> |
||||
<Container className = 'left-column'> |
||||
<Text className = 'date'> |
||||
{ _toDateString(date) } |
||||
</Text> |
||||
<Text> |
||||
{ _toTimeString(time) } |
||||
</Text> |
||||
</Container> |
||||
<Container className = 'right-column'> |
||||
<Text className = 'title'> |
||||
{ title } |
||||
</Text> |
||||
{ |
||||
hideURL || !url ? null : ( |
||||
<Text> |
||||
{ url } |
||||
</Text>) |
||||
} |
||||
{ |
||||
typeof duration === 'number' ? ( |
||||
<Text> |
||||
{ getLocalizedDurationFormatter(duration) } |
||||
</Text>) : null |
||||
} |
||||
</Container> |
||||
<Container className = 'actions'> |
||||
{ elementAfter || null } |
||||
</Container> |
||||
</Container> |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,177 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { appNavigate } from '../../app'; |
||||
import { |
||||
createCalendarClickedEvent, |
||||
createCalendarSelectedEvent, |
||||
sendAnalytics |
||||
} from '../../analytics'; |
||||
import { MeetingsList } from '../../base/react'; |
||||
|
||||
import { isCalendarEnabled } from '../functions'; |
||||
|
||||
import AddMeetingUrlButton from './AddMeetingUrlButton'; |
||||
import JoinButton from './JoinButton'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of |
||||
* {@link CalendarListContent}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The calendar event list. |
||||
*/ |
||||
_eventList: Array<Object>, |
||||
|
||||
/** |
||||
* Indicates if the list is disabled or not. |
||||
*/ |
||||
disabled: boolean, |
||||
|
||||
/** |
||||
* The Redux dispatch function. |
||||
*/ |
||||
dispatch: Function, |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
listEmptyComponent: React$Node, |
||||
}; |
||||
|
||||
/** |
||||
* Component to display a list of events from a connected calendar. |
||||
*/ |
||||
class CalendarListContent extends Component<Props> { |
||||
/** |
||||
* Default values for the component's props. |
||||
*/ |
||||
static defaultProps = { |
||||
_eventList: [] |
||||
}; |
||||
|
||||
/** |
||||
* Initializes a new {@code CalendarListContent} instance. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onJoinPress = this._onJoinPress.bind(this); |
||||
this._onPress = this._onPress.bind(this); |
||||
this._toDisplayableItem = this._toDisplayableItem.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentDidMount()}. Invoked |
||||
* immediately after this component is mounted. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {void} |
||||
*/ |
||||
componentDidMount() { |
||||
sendAnalytics(createCalendarSelectedEvent()); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { disabled, listEmptyComponent } = this.props; |
||||
const { _eventList = [] } = this.props; |
||||
const meetings = _eventList.map(this._toDisplayableItem); |
||||
|
||||
return ( |
||||
<MeetingsList |
||||
disabled = { disabled } |
||||
listEmptyComponent = { listEmptyComponent } |
||||
meetings = { meetings } |
||||
onPress = { this._onPress } /> |
||||
); |
||||
} |
||||
|
||||
_onJoinPress: (Object, string) => Function; |
||||
|
||||
/** |
||||
* Handles the list's navigate action. |
||||
* |
||||
* @private |
||||
* @param {Object} event - The click event. |
||||
* @param {string} url - The url string to navigate to. |
||||
* @returns {void} |
||||
*/ |
||||
_onJoinPress(event, url) { |
||||
event.stopPropagation(); |
||||
|
||||
this._onPress(url, 'calendar.meeting.join'); |
||||
} |
||||
|
||||
_onPress: (string, string) => Function; |
||||
|
||||
/** |
||||
* Handles the list's navigate action. |
||||
* |
||||
* @private |
||||
* @param {string} url - The url string to navigate to. |
||||
* @param {string} analyticsEventName - Тhe name of the analytics event. |
||||
* associated with this action. |
||||
* @returns {void} |
||||
*/ |
||||
_onPress(url, analyticsEventName = 'calendar.meeting.tile') { |
||||
sendAnalytics(createCalendarClickedEvent(analyticsEventName)); |
||||
|
||||
this.props.dispatch(appNavigate(url)); |
||||
} |
||||
|
||||
_toDisplayableItem: Object => Object; |
||||
|
||||
/** |
||||
* Creates a displayable object from an event. |
||||
* |
||||
* @param {Object} event - The calendar event. |
||||
* @private |
||||
* @returns {Object} |
||||
*/ |
||||
_toDisplayableItem(event) { |
||||
return { |
||||
elementAfter: event.url |
||||
? <JoinButton |
||||
onPress = { this._onJoinPress } |
||||
url = { event.url } /> |
||||
: (<AddMeetingUrlButton |
||||
calendarId = { event.calendarId } |
||||
eventId = { event.id } />), |
||||
date: event.startDate, |
||||
time: [ event.startDate, event.endDate ], |
||||
description: event.url, |
||||
title: event.title, |
||||
url: event.url |
||||
}; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps redux state to component props. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @returns {{ |
||||
* _eventList: Array<Object> |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state: Object) { |
||||
return { |
||||
_eventList: state['features/calendar-sync'].events |
||||
}; |
||||
} |
||||
|
||||
export default isCalendarEnabled() |
||||
? connect(_mapStateToProps)(CalendarListContent) |
||||
: undefined; |
@ -0,0 +1,102 @@ |
||||
// @flow
|
||||
import React from 'react'; |
||||
|
||||
import { |
||||
createRecentClickedEvent, |
||||
createRecentSelectedEvent, |
||||
sendAnalytics |
||||
} from '../../analytics'; |
||||
import { appNavigate } from '../../app'; |
||||
import { |
||||
AbstractPage, |
||||
Container, |
||||
Text |
||||
} from '../../base/react'; |
||||
|
||||
import styles from './styles'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link AbstractRecentList} |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The redux store's {@code dispatch} function. |
||||
*/ |
||||
dispatch: Dispatch<*>, |
||||
|
||||
/** |
||||
* The translate function. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* An abstract component for the recent list. |
||||
* |
||||
*/ |
||||
export default class AbstractRecentList<P: Props> extends AbstractPage<P> { |
||||
/** |
||||
* Initializes a new {@code RecentList} instance. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props: P) { |
||||
super(props); |
||||
|
||||
this._onPress = this._onPress.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentDidMount()}. Invoked |
||||
* immediately after this component is mounted. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {void} |
||||
*/ |
||||
componentDidMount() { |
||||
sendAnalytics(createRecentSelectedEvent()); |
||||
} |
||||
|
||||
_getRenderListEmptyComponent: () => React$Node; |
||||
|
||||
/** |
||||
* Returns a list empty component if a custom one has to be rendered instead |
||||
* of the default one in the {@link NavigateSectionList}. |
||||
* |
||||
* @private |
||||
* @returns {React$Component} |
||||
*/ |
||||
_getRenderListEmptyComponent() { |
||||
const { t } = this.props; |
||||
|
||||
return ( |
||||
<Container |
||||
className = 'meetings-list-empty' |
||||
style = { styles.emptyListContainer }> |
||||
<Text |
||||
className = 'description' |
||||
style = { styles.emptyListText }> |
||||
{ t('welcomepage.recentListEmpty') } |
||||
</Text> |
||||
</Container> |
||||
); |
||||
} |
||||
|
||||
_onPress: string => {}; |
||||
|
||||
/** |
||||
* Handles the list's navigate action. |
||||
* |
||||
* @private |
||||
* @param {string} url - The url string to navigate to. |
||||
* @returns {void} |
||||
*/ |
||||
_onPress(url) { |
||||
const { dispatch } = this.props; |
||||
|
||||
sendAnalytics(createRecentClickedEvent('recent.meeting.tile')); |
||||
|
||||
dispatch(appNavigate(url)); |
||||
} |
||||
} |
@ -0,0 +1,99 @@ |
||||
// @flow
|
||||
import React from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { translate } from '../../base/i18n'; |
||||
import { MeetingsList } from '../../base/react'; |
||||
|
||||
import AbstractRecentList from './AbstractRecentList'; |
||||
import { isRecentListEnabled, toDisplayableList } from '../functions'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link RecentList} |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* Renders the list disabled. |
||||
*/ |
||||
disabled: boolean, |
||||
|
||||
/** |
||||
* The redux store's {@code dispatch} function. |
||||
*/ |
||||
dispatch: Dispatch<*>, |
||||
|
||||
/** |
||||
* The translate function. |
||||
*/ |
||||
t: Function, |
||||
|
||||
/** |
||||
* The recent list from the Redux store. |
||||
*/ |
||||
_recentList: Array<Object> |
||||
}; |
||||
|
||||
/** |
||||
* The cross platform container rendering the list of the recently joined rooms. |
||||
* |
||||
*/ |
||||
class RecentList extends AbstractRecentList<Props> { |
||||
_getRenderListEmptyComponent: () => React$Node; |
||||
_onPress: string => {}; |
||||
|
||||
/** |
||||
* Initializes a new {@code RecentList} instance. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this._getRenderListEmptyComponent |
||||
= this._getRenderListEmptyComponent.bind(this); |
||||
this._onPress = this._onPress.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements the React Components's render method. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
if (!isRecentListEnabled()) { |
||||
return null; |
||||
} |
||||
const { |
||||
disabled, |
||||
_recentList |
||||
} = this.props; |
||||
const recentList = toDisplayableList(_recentList); |
||||
|
||||
return ( |
||||
<MeetingsList |
||||
disabled = { disabled } |
||||
hideURL = { true } |
||||
listEmptyComponent = { this._getRenderListEmptyComponent() } |
||||
meetings = { recentList } |
||||
onPress = { this._onPress } /> |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps redux state to component props. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @returns {{ |
||||
* _defaultServerURL: string, |
||||
* _recentList: Array |
||||
* }} |
||||
*/ |
||||
export function _mapStateToProps(state: Object) { |
||||
return { |
||||
_recentList: state['features/recent-list'] |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(RecentList)); |
@ -0,0 +1 @@ |
||||
export default {}; |
@ -1,80 +0,0 @@ |
||||
import { |
||||
getLocalizedDateFormatter, |
||||
getLocalizedDurationFormatter |
||||
} from '../base/i18n'; |
||||
import { parseURIString } from '../base/util'; |
||||
|
||||
/** |
||||
* Creates a displayable list item of a recent list entry. |
||||
* |
||||
* @private |
||||
* @param {Object} item - The recent list entry. |
||||
* @param {string} defaultServerURL - The default server URL. |
||||
* @param {Function} t - The translate function. |
||||
* @returns {Object} |
||||
*/ |
||||
export function toDisplayableItem(item, defaultServerURL, t) { |
||||
const location = parseURIString(item.conference); |
||||
const baseURL = `${location.protocol}//${location.host}`; |
||||
const serverName = baseURL === defaultServerURL ? null : location.host; |
||||
|
||||
return { |
||||
colorBase: serverName, |
||||
id: { |
||||
date: item.date, |
||||
url: item.conference |
||||
}, |
||||
key: `key-${item.conference}-${item.date}`, |
||||
lines: [ |
||||
_toDateString(item.date, t), |
||||
_toDurationString(item.duration), |
||||
serverName |
||||
], |
||||
title: location.room, |
||||
url: item.conference |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Generates a duration string for the item. |
||||
* |
||||
* @private |
||||
* @param {number} duration - The item's duration. |
||||
* @returns {string} |
||||
*/ |
||||
export function _toDurationString(duration) { |
||||
if (duration) { |
||||
return getLocalizedDurationFormatter(duration); |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Generates a date string for the item. |
||||
* |
||||
* @private |
||||
* @param {number} itemDate - The item's timestamp. |
||||
* @param {Function} t - The translate function. |
||||
* @returns {string} |
||||
*/ |
||||
export function _toDateString(itemDate, t) { |
||||
const m = getLocalizedDateFormatter(itemDate); |
||||
const date = new Date(itemDate); |
||||
const dateInMs = date.getTime(); |
||||
const now = new Date(); |
||||
const todayInMs = (new Date()).setHours(0, 0, 0, 0); |
||||
const yesterdayInMs = todayInMs - 86400000; // 1 day = 86400000ms
|
||||
|
||||
if (dateInMs >= todayInMs) { |
||||
return m.fromNow(); |
||||
} else if (dateInMs >= yesterdayInMs) { |
||||
return t('dateUtils.yesterday'); |
||||
} else if (date.getFullYear() !== now.getFullYear()) { |
||||
// We only want to include the year in the date if its not the current
|
||||
// year.
|
||||
return m.format('ddd, MMMM DD h:mm A, gggg'); |
||||
} |
||||
|
||||
return m.format('ddd, MMMM DD h:mm A'); |
||||
} |
@ -0,0 +1,76 @@ |
||||
// @flow
|
||||
import React, { Component } from 'react'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link Tab} |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The index of the tab. |
||||
*/ |
||||
index: number, |
||||
|
||||
/** |
||||
* Indicates if the tab is selected or not. |
||||
*/ |
||||
isSelected: boolean, |
||||
|
||||
/** |
||||
* The label of the tab. |
||||
*/ |
||||
label: string, |
||||
|
||||
/** |
||||
* Handler for selecting the tab. |
||||
*/ |
||||
onSelect: Function |
||||
} |
||||
|
||||
/** |
||||
* A React component that implements tabs. |
||||
* |
||||
*/ |
||||
export default class Tab extends Component<Props> { |
||||
/** |
||||
* Initializes a new {@code Tab} instance. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this._onSelect = this._onSelect.bind(this); |
||||
} |
||||
|
||||
_onSelect: () => {}; |
||||
|
||||
/** |
||||
* Selects a tab. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_onSelect() { |
||||
const { index, onSelect } = this.props; |
||||
|
||||
onSelect(index); |
||||
} |
||||
|
||||
/** |
||||
* Implements the React Components's render method. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { index, isSelected, label } = this.props; |
||||
const className = `tab${isSelected ? ' selected' : ''}`; |
||||
|
||||
return ( |
||||
<div |
||||
className = { className } |
||||
key = { index } |
||||
onClick = { this._onSelect }> |
||||
{ label } |
||||
</div>); |
||||
} |
||||
} |
@ -0,0 +1,63 @@ |
||||
// @flow
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import Tab from './Tab'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link Tabs} |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* Handler for selecting the tab. |
||||
*/ |
||||
onSelect: Function, |
||||
|
||||
/** |
||||
* The index of the selected tab. |
||||
*/ |
||||
selected: number, |
||||
|
||||
/** |
||||
* Tabs information. |
||||
*/ |
||||
tabs: Object |
||||
}; |
||||
|
||||
/** |
||||
* A React component that implements tabs. |
||||
* |
||||
*/ |
||||
export default class Tabs extends Component<Props> { |
||||
/** |
||||
* Implements the React Components's render method. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { onSelect, selected, tabs } = this.props; |
||||
const { content } = tabs[selected]; |
||||
|
||||
return ( |
||||
<div className = 'tab-container'> |
||||
<div className = 'tab-content'> |
||||
{ content } |
||||
</div> |
||||
{ tabs.length > 1 ? ( |
||||
<div className = 'tab-buttons'> |
||||
{ |
||||
tabs.map((tab, index) => ( |
||||
<Tab |
||||
index = { index } |
||||
isSelected = { index === selected } |
||||
key = { index } |
||||
label = { tab.label } |
||||
onSelect = { onSelect } /> |
||||
)) |
||||
} |
||||
</div>) : null |
||||
} |
||||
</div> |
||||
); |
||||
} |
||||
} |
Loading…
Reference in new issue