[I18N] add tabs for navigating between forms (#9364)

This commit is contained in:
Marvin Frachet 2021-02-11 10:35:22 +01:00 committed by GitHub
parent 9fd676e62c
commit 6c23b08920
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 318 additions and 38 deletions

View File

@ -0,0 +1,37 @@
import React from 'react';
import styled from 'styled-components';
import { Flex, Text } from '@buffetjs/core';
export const TabNavRaw = styled(props => <Flex flexDirection="column" {...props} />)`
width: 100%;
`;
export const TabsRaw = styled(props => (
<Flex alignItems="center" justifyContent="flex-end" {...props} />
))`
width: 100%;
margin-left: ${({ position }) => (position === 'right' ? 'auto' : 0)};
border-bottom: 1px solid ${({ theme }) => theme.main.colors.brightGrey};
`;
export const TabButton = styled(props => (
<Text
as="button"
textTransform="uppercase"
fontSize="sm"
fontWeight={props['aria-selected'] ? 'bold' : 'semiBold'}
color={props['aria-selected'] ? 'mediumBlue' : 'grey'}
{...props}
/>
))`
height: 3.8rem;
letter-spacing: 0.7px;
margin-left: 3rem;
border-bottom: 2px solid
${props => (props['aria-selected'] ? props.theme.main.colors.mediumBlue : 'transparent')};
padding: 0;
`;
export const TabPanelRaw = styled.div`
padding: 2.2rem 0;
`;

View File

@ -0,0 +1,129 @@
import React, { createContext, useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { TabNavRaw, TabButton, TabsRaw, TabPanelRaw } from './TabComponents';
const TabsIndexContext = createContext({ selectedIndex: 0, setSelectedIndex: () => undefined });
const TabsIdContext = createContext(null);
export const TabsNav = ({ children, defaultSelection, label, id }) => {
const [selectedIndex, setSelectedIndex] = useState(defaultSelection);
return (
<TabsIdContext.Provider value={id}>
<TabsIndexContext.Provider value={{ selectedIndex, setSelectedIndex }}>
<TabNavRaw role="tablist" aria-label={label}>
{children}
</TabNavRaw>
</TabsIndexContext.Provider>
</TabsIdContext.Provider>
);
};
TabsNav.propTypes = {
id: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
label: PropTypes.string.isRequired,
defaultSelection: PropTypes.number.isRequired,
};
export const Tabs = ({ children, position }) => {
const id = useContext(TabsIdContext);
const { setSelectedIndex, selectedIndex } = useContext(TabsIndexContext);
const childrenArray = React.Children.toArray(children);
return (
<TabsRaw position={position}>
{childrenArray.map((child, index) =>
React.cloneElement(child, {
onSelect: () => setSelectedIndex(index),
selected: index === selectedIndex,
id: `${id}-${index}`,
})
)}
</TabsRaw>
);
};
Tabs.defaultProps = {
position: 'left',
};
Tabs.propTypes = {
children: PropTypes.node.isRequired,
position: PropTypes.oneOf(['left', 'right']),
};
export const TabsPanel = ({ children }) => {
const { selectedIndex } = useContext(TabsIndexContext);
const id = useContext(TabsIdContext);
const childrenArray = React.Children.toArray(children);
return (
<>
{childrenArray.map((child, index) =>
React.cloneElement(child, { selected: index === selectedIndex, id: `${id}-${index}` })
)}
</>
);
};
TabsPanel.propTypes = {
children: PropTypes.node.isRequired,
};
export const Tab = ({ children, selected, onSelect, id }) => {
const ariaControls = `${id}-tabpanel`;
return (
<TabButton
role="tab"
id={`${id}-tab`}
aria-selected={selected}
aria-controls={ariaControls}
tabIndex={-1}
onClick={onSelect}
type="button"
>
{children}
</TabButton>
);
};
Tab.defaultProps = {
selected: false,
id: '',
onSelect: () => undefined,
};
Tab.propTypes = {
children: PropTypes.node.isRequired,
selected: PropTypes.bool,
onSelect: PropTypes.func,
id: PropTypes.string,
};
export const TabPanel = ({ children, selected, id }) => {
const labelledBy = `${id}-tab`;
return (
<TabPanelRaw
role="tabpanel"
aria-labelledby={labelledBy}
hidden={!selected}
id={`${id}-tabpanel`}
>
{children}
</TabPanelRaw>
);
};
TabPanel.defaultProps = {
id: '',
selected: false,
};
TabPanel.propTypes = {
children: PropTypes.node.isRequired,
selected: PropTypes.bool,
id: PropTypes.string,
};

View File

@ -25,6 +25,7 @@ export { default as HeaderSearch } from './components/HeaderSearch';
export { default as IcoContainer } from './components/IcoContainer';
export { default as InputAddon } from './components/InputAddon';
export { default as EmptyState } from './components/EmptyState';
export * from './components/Tabs';
export { default as InputAddonWithErrors } from './components/InputAddonWithErrors';
export { default as InputCheckbox } from './components/InputCheckbox';

View File

@ -30,12 +30,15 @@ const LocaleSettingsPage = ({ locale, onDelete, onEdit }) => {
<FontAwesomeIcon icon="trash-alt" />
</span>
) : null,
onClick: () => onDelete(locale),
onClick: e => {
e.stopPropagation();
onDelete(locale);
},
});
}
return (
<CustomRow>
<CustomRow onClick={() => onEdit(locale)}>
<td>
<Text>{locale.code}</Text>
</td>

View File

@ -0,0 +1,73 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Label } from '@buffetjs/core';
import { Inputs } from '@buffetjs/custom';
import Select from 'react-select';
import { Col, Row } from 'reactstrap';
import { useIntl } from 'react-intl';
import { useFormikContext } from 'formik';
import { getTrad } from '../../utils';
const BaseForm = ({ options, defaultOption }) => {
const { formatMessage } = useIntl();
const { values, handleChange } = useFormikContext();
return (
<Row>
<Col>
<span id="locale-code">
<Label htmlFor="">
{formatMessage({
id: getTrad('Settings.locales.modal.edit.locales.label'),
})}
</Label>
</span>
<Select
aria-labelledby="locale-code"
options={options}
defaultValue={defaultOption}
isDisabled
/>
</Col>
<Col>
<Inputs
label={formatMessage({
id: getTrad('Settings.locales.modal.edit.locales.displayName'),
})}
name="displayName"
description={formatMessage({
id: getTrad('Settings.locales.modal.edit.locales.displayName.description'),
})}
type="text"
value={values.displayName}
onChange={handleChange}
validations={{
max: 50,
}}
translatedErrors={{
max: formatMessage({
id: getTrad('Settings.locales.modal.edit.locales.displayName.error'),
}),
}}
/>
</Col>
</Row>
);
};
BaseForm.defaultProps = {
defaultOption: undefined,
};
BaseForm.propTypes = {
options: PropTypes.arrayOf(
PropTypes.exact({ value: PropTypes.number.isRequired, label: PropTypes.string.isRequired })
).isRequired,
defaultOption: PropTypes.exact({
value: PropTypes.number.isRequired,
label: PropTypes.string.isRequired,
}),
};
export default BaseForm;

View File

@ -1,13 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalHeader, ModalSection, ModalFooter } from 'strapi-helper-plugin';
import {
Modal,
ModalHeader,
HeaderModal,
HeaderModalTitle,
ModalFooter,
ModalForm,
Tabs,
TabsNav,
Tab,
TabsPanel,
TabPanel,
} from 'strapi-helper-plugin';
import { useIntl } from 'react-intl';
import { Button, Label, InputText } from '@buffetjs/core';
import Select from 'react-select';
import { Button } from '@buffetjs/core';
import { Formik } from 'formik';
import { object, string } from 'yup';
import useEditLocale from '../../hooks/useEditLocale';
import { getTrad } from '../../utils';
import BaseForm from './BaseForm';
const ModalEdit = ({ localeToEdit, onClose, locales }) => {
const { isEditing, editLocale } = useEditLocale();
@ -31,6 +43,12 @@ const ModalEdit = ({ localeToEdit, onClose, locales }) => {
return (
<Modal isOpen={isOpened} onToggle={onClose}>
<HeaderModal>
<ModalHeader
headerBreadcrumbs={[formatMessage({ id: getTrad('Settings.list.actions.edit') })]}
/>
</HeaderModal>
<Formik
initialValues={{ displayName: localeToEdit ? localeToEdit.name : '' }}
onSubmit={handleSubmit}
@ -38,44 +56,49 @@ const ModalEdit = ({ localeToEdit, onClose, locales }) => {
displayName: string().max(50, 'Settings.locales.modal.edit.locales.displayName.error'),
})}
>
{({ values, handleSubmit, handleChange, errors }) => (
{({ handleSubmit, errors }) => (
<form onSubmit={handleSubmit}>
<ModalHeader
headerBreadcrumbs={[formatMessage({ id: getTrad('Settings.list.actions.edit') })]}
/>
<ModalSection>
<div>
<span id="locale-code">
<Label htmlFor="">
{formatMessage({ id: getTrad('Settings.locales.modal.edit.locales.label') })}
</Label>
</span>
<Select
aria-labelledby="locale-code"
options={options}
defaultValue={defaultOption}
isDisabled
/>
</div>
<div>
<Label htmlFor="displayName">
<div className="container-fluid">
<div className="container-fluid">
<HeaderModalTitle
style={{
fontSize: '1.8rem',
height: '65px',
fontWeight: 'bold',
alignItems: 'center',
marginBottom: '-39px',
paddingTop: '16px',
}}
>
{formatMessage({
id: getTrad('Settings.locales.modal.edit.locales.displayName'),
id: getTrad('Settings.locales.modal.title'),
})}
</Label>
<InputText name="displayName" value={values.displayName} onChange={handleChange} />
</HeaderModalTitle>
{errors.displayName && (
<small>
{formatMessage({
id: getTrad(' Settings.locales.modal.edit.locales.displayName.error'),
<ModalForm>
<TabsNav
defaultSelection={0}
label={formatMessage({
id: getTrad('Settings.locales.modal.edit.tab.label'),
})}
</small>
)}
id="i18n-settings-tabs"
>
<Tabs position="right">
<Tab>{formatMessage({ id: getTrad('Settings.locales.modal.base') })}</Tab>
<Tab>{formatMessage({ id: getTrad('Settings.locales.modal.advanced') })}</Tab>
</Tabs>
<TabsPanel>
<TabPanel>
<BaseForm options={options} defaultOption={defaultOption} />
</TabPanel>
<TabPanel>advanced</TabPanel>
</TabsPanel>
</TabsNav>
</ModalForm>
</div>
</ModalSection>
</div>
<ModalFooter>
<section>
<Button type="button" color="cancel" onClick={onClose}>

View File

@ -30,7 +30,15 @@ jest.mock('strapi-helper-plugin', () => ({
ModalHeader: ({ children }) => <div>{children}</div>,
ModalSection: ({ children }) => <div>{children}</div>,
ModalFooter: ({ children }) => <div>{children}</div>,
HeaderModal: ({ children }) => <div>{children}</div>,
HeaderModalTitle: ({ children }) => <div>{children}</div>,
ModalForm: ({ children }) => <div>{children}</div>,
ListButton: () => <div />,
Tabs: ({ children }) => <div>{children}</div>,
TabsNav: ({ children }) => <div>{children}</div>,
Tab: ({ children }) => <div>{children}</div>,
TabsPanel: ({ children }) => <div>{children}</div>,
TabPanel: ({ children }) => <div>{children}</div>,
useUserPermissions: jest.fn(),
request: jest.fn(),
}));
@ -166,6 +174,7 @@ describe('i18n settings page', () => {
'a very very very very long string that has more than fifty characters in order to show a warning',
},
});
fireEvent.blur(screen.getByLabelText('Settings.locales.modal.edit.locales.displayName'));
await waitFor(() =>
expect(screen.getByText('Settings.locales.modal.edit.confirmation')).toBeDisabled()

View File

@ -23,6 +23,7 @@
"Settings.locales.list.title.singular": "{number} Locale",
"Settings.locales.list.title.plural": "{number} Locales",
"Settings.locales.row.default-locale": "Default locale",
"Settings.locales.modal.title": "Configurations",
"Settings.locales.modal.delete.confirm": "Yes, delete",
"Settings.locales.modal.delete.message": "Deleting this locale will delete all associated content. If you want to keep some content, make sure to reallocate it to another locale first.",
"Settings.locales.modal.delete.secondMessage": "Do you want to delete this locale?",
@ -31,5 +32,9 @@
"Settings.locales.modal.edit.success": "Locale successfully edited",
"Settings.locales.modal.edit.locales.label": "Locales",
"Settings.locales.modal.edit.locales.displayName": "Locale display name",
"Settings.locales.modal.edit.locales.displayName.error": "The locale display name can only be less than 50 characters."
"Settings.locales.modal.edit.locales.displayName.error": "The locale display name can only be less than 50 characters.",
"Settings.locales.modal.edit.locales.displayName.description":"Locale will be displayed under that name in the administration panel",
"Settings.locales.modal.edit.tab.label":"Navigating between the I18N base settings and advanced settings",
"Settings.locales.modal.base": "Base settings",
"Settings.locales.modal.advanced": "Advanced settings"
}