mirror of https://github.com/openedx/paragon.git
feat!: implement i18n in Paragon components and in docs site (#1100)
Adds support for i18n translations in Paragon components, including the commands to push/pull translations from Transifex in an automated process. This change itself does not guarantee the components are truly translated yet as they may still need a translator and/or a reviewer to approve the translation in Transifex before a translation is pulled. To upgrade, ensure you are using at least `react-intl@5.25.0` in your application. BREAKING CHANGE: By adding i18n support to the Paragon design system, we are introducing a peer dependency on `react-intl@5.25.0` or greater. This may be a breaking change for some consumers, if your repository: * Uses v1 of `@edx/frontend-platform` * Uses older version of `react-intl` than v5.25.0 directly. * Does not use `react-intl`.
This commit is contained in:
parent
aecdf1f211
commit
53e0ac632b
|
|
@ -5,6 +5,7 @@ npm-debug.log
|
|||
coverage
|
||||
jest*
|
||||
dist
|
||||
src/i18n/transifex_input.json
|
||||
|
||||
# gatsby files
|
||||
www/.cache/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[edx-platform.paragon]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = STRUCTURED_JSON
|
||||
43
Makefile
43
Makefile
|
|
@ -7,3 +7,46 @@ build:
|
|||
rm -rf dist/__mocks__
|
||||
rm -rf dist/setupTest.js
|
||||
node build-scss.js
|
||||
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
.PHONY: test
|
||||
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
|
||||
|
||||
.PHONY: test.npm.*
|
||||
test.npm.%: validate-no-uncommitted-package-lock-changes
|
||||
test -d node_modules || $(MAKE) requirements
|
||||
npm run $(*)
|
||||
|
||||
.PHONY: requirements
|
||||
requirements: ## install ci requirements
|
||||
npm ci
|
||||
|
||||
i18n.extract:
|
||||
# Pulling display strings from .jsx files into .json files...
|
||||
npm run-script i18n_extract
|
||||
|
||||
extract_translations: | requirements i18n.extract
|
||||
|
||||
# Despite the name, we actually need this target to detect changes in the incoming translated message files as well.
|
||||
detect_changed_source_translations:
|
||||
# Checking for changed translations...
|
||||
git diff --exit-code $(i18n)
|
||||
|
||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||
push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --language=$(transifex_langs)
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
# Checking for package-lock.json changes...
|
||||
git diff --exit-code package-lock.json
|
||||
|
|
|
|||
134
README.md
134
README.md
|
|
@ -167,6 +167,140 @@ JSX code blocks in the markdown file can be made interactive with the live attri
|
|||
|
||||
Visit the documentation at [http://localhost:8000](http://localhost:8000) and navigate to see your README.md powered page and workbench. Changes to the README.md file will auto refresh the page.
|
||||
|
||||
### Internationalization
|
||||
|
||||
Paragon supports internationalization for its components out of the box with the support of [react-intl](https://formatjs.io/docs/react-intl/). You can view translated strings for each component on the docs site after switching language on a settings tab.
|
||||
|
||||
#### For consumers
|
||||
|
||||
Since we are using ``react-intl`` that means that your whole app needs to be wrapped in its context, e.g.
|
||||
```javascript
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { messages as paragonMessages } from '@edx/paragon';
|
||||
|
||||
ReactDOM.render(
|
||||
<IntlProvider locale={usersLocale} messages={paragonMessages[usersLocale]}>
|
||||
<App />
|
||||
</IntlProvider>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
```
|
||||
|
||||
Note that if you are using ``@edx/frontend-platform``'s ``AppProvider`` component you don't need a separate context,
|
||||
you would only need to add Paragon's messages like this
|
||||
|
||||
```javascript
|
||||
import { APP_READY, subscribe, initialize } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { messages as paragonMessages } from '@edx/paragon';
|
||||
import App from './App';
|
||||
// this is your app's i18n messages
|
||||
import appMessages from './i18n';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider>
|
||||
<App />
|
||||
</AppProvider>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
})
|
||||
|
||||
initialize({
|
||||
// this will add your app's messages as well as Paragon's messages to your app
|
||||
messages: [
|
||||
appMessages,
|
||||
paragonMessages,
|
||||
],
|
||||
// here you will typically provide other configurations for you app
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
#### For developers
|
||||
|
||||
When developing a new component you should generally follow three rules:
|
||||
1. The component should not have **any** hardcoded strings as it would be impossible for consumers to translate it
|
||||
2. Internationalize all default values of props that expect strings, i.e.
|
||||
|
||||
- For places where you need to display a string, and it's okay if it is a React element use ``FormattedMessage``, e.g. (see [Alert](src/Alert/index.jsx) component for a full example)
|
||||
|
||||
```javascript
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
<FormattedMessage
|
||||
id="pgn.Alert.closeLabel"
|
||||
defaultMessage="Dismiss"
|
||||
description="Label of a close button on Alert component"
|
||||
/>
|
||||
```
|
||||
|
||||
- For places where the display string has to be a plain JavaScript string use ``formatMessage``, this would require access to ``intl`` object from ``react-intl``, e.g.
|
||||
|
||||
- For class components use ``injectIntl`` HOC
|
||||
|
||||
```javascript
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
class MyClassComponent extends React.Component {
|
||||
render() {
|
||||
const { altText, intl } = this.props;
|
||||
const intlAltText = altText || intl.formatMessage({
|
||||
id: 'pgn.MyComponent.altText',
|
||||
defaultMessage: 'Close',
|
||||
description: 'Close label for Toast component',
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
alt={intlCloseLabel}
|
||||
onClick={() => {}}
|
||||
variant="primary"
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(MyClassComponent);
|
||||
```
|
||||
|
||||
- For functional components use ``useIntl`` hook
|
||||
|
||||
```javascript
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const MyFunctionComponent = ({ altText }) => {
|
||||
const intls = useIntl();
|
||||
const intlAltText = altText || intl.formatMessage({
|
||||
id: 'pgn.MyComponent.altText',
|
||||
defaultMessage: 'Close',
|
||||
description: 'Close label for Toast component',
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
alt={intlCloseLabel}
|
||||
onClick={() => {}}
|
||||
variant="primary"
|
||||
/>
|
||||
)
|
||||
|
||||
export default MyFunctionComponent;
|
||||
```
|
||||
|
||||
**Notes on the format above**:
|
||||
- `id` is required and must be a dot-separated string of the format `pgn.<componentName>.<subcomponentName>.<propName>`
|
||||
- The `defaultMessage` is required, and should be the English display string.
|
||||
- The `description` is optional, but highly recommended, this text gives context to translators about the string.
|
||||
|
||||
|
||||
3. If your component expects a string as a prop, allow the prop to also be an element since consumers may want to also pass instance of their own translated string, for example you might define a string prop like this:
|
||||
```javascript
|
||||
MyComponent.PropTypes = {
|
||||
myProp: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
};
|
||||
```
|
||||
|
||||
### Developing locally against MFE
|
||||
|
||||
If you want to test the changes with local MFE setup, you need to create a "module.config.js" file in your MFE's directory containing local module overrides. After that the webpack build for your application will automatically pick your local version of Paragon and use it. The example of module.config.js file looks like this (for more details about module.config.js, refer to the [frontend-build documentation](https://github.com/edx/frontend-build#local-module-configuration-for-webpack).):
|
||||
|
|
|
|||
|
|
@ -56,10 +56,10 @@ Consequences
|
|||
ReactDOM.render(
|
||||
<AppProvider>
|
||||
<App />
|
||||
</IntlProvider>,
|
||||
</AppProvider>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
initialize({
|
||||
// this will add your app's messages as well as Paragon's messages to your app
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -18,7 +18,7 @@
|
|||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"@edx/brand-openedx": "^1.1.0",
|
||||
"@edx/frontend-platform": "^1.15.6",
|
||||
"@edx/frontend-platform": "^2.0.0",
|
||||
"core-js": "^3.22.2",
|
||||
"regenerator-runtime": "^0.13.9"
|
||||
},
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -39,7 +39,8 @@
|
|||
"test:watch": "npm run test -- --watch",
|
||||
"generate-component": "cd component-generator && npm start",
|
||||
"generate-component-lint": "cd component-generator && npm run lint",
|
||||
"generate-changelog": "node generate-changelog.js"
|
||||
"generate-changelog": "node generate-changelog.js",
|
||||
"i18n_extract": "formatjs extract 'src/**/*.jsx' --out-file ./src/i18n/transifex_input.json --format transifex"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
|
|
@ -64,7 +65,8 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.6 || ^17.0.0",
|
||||
"react-dom": "^16.8.6 || ^17.0.0"
|
||||
"react-dom": "^16.8.6 || ^17.0.0",
|
||||
"react-intl": "^5.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.16.8",
|
||||
|
|
@ -74,6 +76,7 @@
|
|||
"@babel/preset-env": "^7.16.8",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"@edx/eslint-config": "^3.0.0",
|
||||
"@formatjs/cli": "^4.8.4",
|
||||
"@semantic-release/changelog": "^5.0.1",
|
||||
"@semantic-release/git": "^9.0.1",
|
||||
"@testing-library/jest-dom": "^5.16.3",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import renderer, { act } from 'react-test-renderer';
|
||||
import { Context as ResponsiveContext } from 'react-responsive';
|
||||
import breakpoints from '../utils/breakpoints';
|
||||
|
|
@ -7,56 +8,65 @@ import Button from '../Button';
|
|||
import Alert from './index';
|
||||
import { Info } from '../../icons';
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const AlertWrapper = ({ children, ...props }) => (
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<Alert {...props}>
|
||||
{children}
|
||||
</Alert>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<Alert />', () => {
|
||||
it('renders without any props', () => {
|
||||
const tree = renderer.create((
|
||||
<Alert>Alert</Alert>
|
||||
<AlertWrapper>Alert</AlertWrapper>
|
||||
)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('renders with icon prop', () => {
|
||||
const tree = renderer.create((
|
||||
<Alert icon={Info}>Alert</Alert>
|
||||
<AlertWrapper icon={Info}>Alert</AlertWrapper>
|
||||
)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('renders with dismissible prop', () => {
|
||||
const tree = renderer.create((
|
||||
<Alert dismissible>Alert</Alert>
|
||||
<AlertWrapper dismissible>Alert</AlertWrapper>
|
||||
)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('handles dismissible onClose', () => {
|
||||
const mockOnClose = jest.fn();
|
||||
const wrapper = mount((
|
||||
<Alert onClose={mockOnClose} dismissible>Alert</Alert>
|
||||
<AlertWrapper onClose={mockOnClose} dismissible>Alert</AlertWrapper>
|
||||
));
|
||||
wrapper.find('.btn').simulate('click');
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('renders with button prop', () => {
|
||||
const tree = renderer.create((
|
||||
<Alert actions={[<Button>Hello</Button>]}>Alert</Alert>
|
||||
<AlertWrapper actions={[<Button>Hello</Button>]}>Alert</AlertWrapper>
|
||||
)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('handles button onClick', () => {
|
||||
const mockOnClick = jest.fn();
|
||||
const wrapper = mount((
|
||||
<Alert actions={[<Button onClick={mockOnClick}>Hello</Button>]}>Alert</Alert>
|
||||
<AlertWrapper actions={[<Button onClick={mockOnClick}>Hello</Button>]}>Alert</AlertWrapper>
|
||||
));
|
||||
wrapper.find('.btn').simulate('click');
|
||||
expect(mockOnClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('renders with button and dismissible props', () => {
|
||||
const tree = renderer.create((
|
||||
<Alert actions={[<Button>Hello</Button>]} dismissible>Alert</Alert>
|
||||
<AlertWrapper actions={[<Button>Hello</Button>]} dismissible>Alert</AlertWrapper>
|
||||
)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('renders with stacked prop', () => {
|
||||
const tree = renderer.create((
|
||||
<Alert stacked actions={[<Button>Hello</Button>]} dismissible>Alert</Alert>
|
||||
<AlertWrapper stacked actions={[<Button>Hello</Button>]} dismissible>Alert</AlertWrapper>
|
||||
)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
|
@ -65,7 +75,7 @@ describe('<Alert />', () => {
|
|||
act(() => {
|
||||
tree = renderer.create((
|
||||
<ResponsiveContext.Provider value={{ width: breakpoints.extraSmall.maxWidth }}>
|
||||
<Alert dismissible>Alert</Alert>
|
||||
<AlertWrapper dismissible>Alert</AlertWrapper>
|
||||
</ResponsiveContext.Provider>
|
||||
)).toJSON();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
import BaseAlert from 'react-bootstrap/Alert';
|
||||
import divWithClassName from 'react-bootstrap/divWithClassName';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
import { Icon } from '..';
|
||||
import breakpoints from '../utils/breakpoints';
|
||||
|
|
@ -66,7 +67,13 @@ const Alert = React.forwardRef(({
|
|||
variant="tertiary"
|
||||
onClick={onClose}
|
||||
>
|
||||
{closeLabel}
|
||||
{closeLabel || (
|
||||
<FormattedMessage
|
||||
id="pgn.Alert.closeLabel"
|
||||
defaultMessage="Dismiss"
|
||||
description="Label of a close button on Alert component"
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{actions && actions.map(cloneActionElement)}
|
||||
|
|
@ -122,8 +129,8 @@ Alert.propTypes = {
|
|||
actions: PropTypes.arrayOf(PropTypes.element),
|
||||
/** Position of the dismiss and call-to-action buttons. Defaults to ``false``. */
|
||||
stacked: PropTypes.bool,
|
||||
/** Sets the text for alert close button. */
|
||||
closeLabel: PropTypes.string,
|
||||
/** Sets the text for alert close button, defaults to 'Dismiss'. */
|
||||
closeLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
};
|
||||
|
||||
Alert.defaultProps = {
|
||||
|
|
@ -133,7 +140,7 @@ Alert.defaultProps = {
|
|||
actions: undefined,
|
||||
dismissible: false,
|
||||
onClose: () => {},
|
||||
closeLabel: ALERT_CLOSE_LABEL_TEXT,
|
||||
closeLabel: undefined,
|
||||
show: true,
|
||||
stacked: false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import SidebarFilters from './SidebarFilters';
|
|||
import DataTableContext from './DataTableContext';
|
||||
|
||||
const DataTableLayout = ({
|
||||
filtersTitle,
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
|
|
@ -15,7 +16,7 @@ const DataTableLayout = ({
|
|||
<div className={classNames('pgn__data-table-layout-wrapper', className)}>
|
||||
{(showFiltersInSidebar && setFilter) && (
|
||||
<div className="pgn__data-table-layout-sidebar">
|
||||
<SidebarFilters />
|
||||
<SidebarFilters title={filtersTitle} />
|
||||
</div>
|
||||
)}
|
||||
<div className="pgn__data-table-layout-main">
|
||||
|
|
@ -27,11 +28,13 @@ const DataTableLayout = ({
|
|||
|
||||
DataTableLayout.defaultProps = {
|
||||
className: null,
|
||||
filtersTitle: undefined,
|
||||
};
|
||||
|
||||
DataTableLayout.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
filtersTitle: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
};
|
||||
|
||||
export default DataTableLayout;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,27 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Button } from '..';
|
||||
|
||||
const ExpandAll = ({ getToggleAllRowsExpandedProps, isAllRowsExpanded }) => (
|
||||
<span {...getToggleAllRowsExpandedProps()}>
|
||||
{isAllRowsExpanded
|
||||
? <Button variant="link" size="inline">Collapse all</Button>
|
||||
: <Button variant="link" size="inline">Expand all</Button>}
|
||||
{isAllRowsExpanded ? (
|
||||
<Button variant="link" size="inline">
|
||||
<FormattedMessage
|
||||
id="pgn.DataTable.ExpandAll.collapseAllLabel"
|
||||
defaultMessage="Collapse all"
|
||||
description="Label of an action button that collapses all expandable rows of DataTable."
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="link" size="inline">
|
||||
<FormattedMessage
|
||||
id="pgn.DataTable.ExpandAll.expandAllLabel"
|
||||
defaultMessage="Expand all"
|
||||
description="Label of an action button that expands all expandable rows of DataTable."
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Button } from '..';
|
||||
import DataTableContext from './DataTableContext';
|
||||
|
||||
|
|
@ -23,7 +24,15 @@ const FilterStatus = ({
|
|||
size={size}
|
||||
onClick={() => setAllFilters([])}
|
||||
>
|
||||
{clearFiltersText}
|
||||
{clearFiltersText === undefined
|
||||
? (
|
||||
<FormattedMessage
|
||||
id="pgn.DataTable.FilterStatus.clearFiltersText"
|
||||
defaultMessage="Clear filters"
|
||||
description="A text that appears on the `Clear filters` button"
|
||||
/>
|
||||
)
|
||||
: clearFiltersText}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -38,8 +47,8 @@ FilterStatus.defaultProps = {
|
|||
variant: 'link',
|
||||
/** The size of the `FilterStatus`. */
|
||||
size: 'inline',
|
||||
/** A text that appears on the `Clear filters` button. */
|
||||
clearFiltersText: 'Clear Filters',
|
||||
/** A text that appears on the `Clear filters` button, defaults to 'Clear filters'. */
|
||||
clearFiltersText: undefined,
|
||||
/** Whether to display applied filters. */
|
||||
showFilteredFields: true,
|
||||
};
|
||||
|
|
@ -49,7 +58,7 @@ FilterStatus.propTypes = {
|
|||
buttonClassName: PropTypes.string,
|
||||
variant: PropTypes.string,
|
||||
size: PropTypes.string,
|
||||
clearFiltersText: PropTypes.string,
|
||||
clearFiltersText: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
|
||||
showFilteredFields: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,39 @@
|
|||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import DataTableContext from './DataTableContext';
|
||||
|
||||
const RowStatus = ({ className }) => {
|
||||
const RowStatus = ({ className, statusText }) => {
|
||||
const { page, rows, itemCount } = useContext(DataTableContext);
|
||||
const pageSize = page?.length || rows?.length;
|
||||
|
||||
if (!pageSize) {
|
||||
return null;
|
||||
}
|
||||
return (<div className={className}>Showing {pageSize} of {itemCount}.</div>);
|
||||
return (
|
||||
<div className={className}>
|
||||
{statusText || (
|
||||
<FormattedMessage
|
||||
id="pgn.DataTable.RowStatus.statusText"
|
||||
defaultMessage="Showing {pageSize} of {itemCount}."
|
||||
description="A text describing how many rows is shown in the table"
|
||||
values={{ itemCount, pageSize }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RowStatus.propTypes = {
|
||||
/** Specifies class name to append to the base element. */
|
||||
className: PropTypes.string,
|
||||
/** A text describing how many rows is shown in the table. */
|
||||
statusText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
};
|
||||
|
||||
RowStatus.defaultProps = {
|
||||
className: undefined,
|
||||
statusText: undefined,
|
||||
};
|
||||
|
||||
export default RowStatus;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,25 @@
|
|||
import React, { useContext, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import DataTableContext from './DataTableContext';
|
||||
import FilterStatus from './FilterStatus';
|
||||
|
||||
const SidebarFilters = () => {
|
||||
const SidebarFilters = ({ title }) => {
|
||||
const { state, columns } = useContext(DataTableContext);
|
||||
const availableFilters = useMemo(() => columns.filter((column) => column.canFilter), [columns]);
|
||||
const filtersApplied = state?.filters && state.filters.length > 0;
|
||||
return (
|
||||
<div className="pgn__data-table-side-filters">
|
||||
<h3 className="pgn__data-table-side-filters-title">Filters</h3>
|
||||
<h3 className="pgn__data-table-side-filters-title">
|
||||
{title || (
|
||||
<FormattedMessage
|
||||
id="pgn.DataTable.SidebarFilters.title"
|
||||
defaultMessage="Filters"
|
||||
description="Title for the sidebar filters component"
|
||||
/>
|
||||
)}
|
||||
</h3>
|
||||
<hr />
|
||||
{availableFilters.map(column => (
|
||||
<div
|
||||
|
|
@ -32,4 +42,13 @@ const SidebarFilters = () => {
|
|||
);
|
||||
};
|
||||
|
||||
SidebarFilters.propTypes = {
|
||||
/** Specifies the title to show near the filters, default to 'Filters'. */
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
};
|
||||
|
||||
SidebarFilters.defaultProps = {
|
||||
title: undefined,
|
||||
};
|
||||
|
||||
export default SidebarFilters;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Button } from '../..';
|
||||
import DataTableContext from '../DataTableContext';
|
||||
|
|
@ -14,12 +15,31 @@ const BaseSelectionStatus = ({
|
|||
numSelectedRows,
|
||||
onSelectAll,
|
||||
onClear,
|
||||
selectAllText,
|
||||
allSelectedText,
|
||||
selectedText,
|
||||
}) => {
|
||||
const { itemCount } = useContext(DataTableContext);
|
||||
const isAllRowsSelected = numSelectedRows === itemCount;
|
||||
const intlAllSelectedText = allSelectedText || (
|
||||
<FormattedMessage
|
||||
id="pgn.DataTable.BaseSelectionStatus.allSelectedText"
|
||||
defaultMessage="All {numSelectedRows} selected"
|
||||
description="Text for all selected label"
|
||||
values={{ numSelectedRows }}
|
||||
/>
|
||||
);
|
||||
const intlSelectedText = selectedText || (
|
||||
<FormattedMessage
|
||||
id="pgn.DataTable.BaseSelectionStatus.selectedText"
|
||||
defaultMessage="{numSelectedRows} selected"
|
||||
description="Text for selected label"
|
||||
values={{ numSelectedRows }}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className={className}>
|
||||
<span>{isAllRowsSelected && 'All '}{numSelectedRows} selected </span>
|
||||
<span>{isAllRowsSelected ? intlAllSelectedText : intlSelectedText}</span>
|
||||
{!isAllRowsSelected && (
|
||||
<Button
|
||||
className={SELECT_ALL_TEST_ID}
|
||||
|
|
@ -27,7 +47,14 @@ const BaseSelectionStatus = ({
|
|||
size="inline"
|
||||
onClick={onSelectAll}
|
||||
>
|
||||
Select all {itemCount}
|
||||
{selectAllText || (
|
||||
<FormattedMessage
|
||||
id="pgn.DataTable.BaseSelectionStatus.selectAllText"
|
||||
defaultMessage="Select all {itemCount}"
|
||||
description="A label for select all button."
|
||||
values={{ itemCount }}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{numSelectedRows > 0 && (
|
||||
|
|
@ -37,7 +64,13 @@ const BaseSelectionStatus = ({
|
|||
size="inline"
|
||||
onClick={onClear}
|
||||
>
|
||||
{clearSelectionText}
|
||||
{clearSelectionText || (
|
||||
<FormattedMessage
|
||||
id="pgn.DataTable.BaseSelectionStatus.clearSelectionText"
|
||||
defaultMessage="Clear selection"
|
||||
description="A label of clear all selection button."
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -46,14 +79,29 @@ const BaseSelectionStatus = ({
|
|||
|
||||
BaseSelectionStatus.defaultProps = {
|
||||
className: undefined,
|
||||
selectAllText: undefined,
|
||||
allSelectedText: undefined,
|
||||
selectedText: undefined,
|
||||
clearSelectionText: undefined,
|
||||
};
|
||||
|
||||
BaseSelectionStatus.propTypes = {
|
||||
/** A class name to append to the base element */
|
||||
className: PropTypes.string,
|
||||
clearSelectionText: PropTypes.string.isRequired,
|
||||
/** A text that appears on the `Clear selection` button, defaults to 'Clear selection' */
|
||||
clearSelectionText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
/** Count of selected rows in the table. */
|
||||
numSelectedRows: PropTypes.number.isRequired,
|
||||
/** A handler for 'Select all' button. */
|
||||
onSelectAll: PropTypes.func.isRequired,
|
||||
/** A handler for 'Clear selection' button. */
|
||||
onClear: PropTypes.func.isRequired,
|
||||
/** A text that appears on the `Select all` button, defaults to 'Select All {itemCount}' */
|
||||
selectAllText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
/** A text that appears when all items have been selected, defaults to 'All {numSelectedRows} selected' */
|
||||
allSelectedText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
/** A text that appears when some items have been selected, defaults to '{numSelectedRows} selected' */
|
||||
selectedText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
};
|
||||
|
||||
export default BaseSelectionStatus;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import {
|
|||
getUnselectedPageRows,
|
||||
getRowIds,
|
||||
} from './data/helpers';
|
||||
import { CLEAR_SELECTION_TEXT } from './data/constants';
|
||||
|
||||
const ControlledSelectionStatus = ({ className, clearSelectionText }) => {
|
||||
const {
|
||||
|
|
@ -49,12 +48,14 @@ const ControlledSelectionStatus = ({ className, clearSelectionText }) => {
|
|||
|
||||
ControlledSelectionStatus.defaultProps = {
|
||||
className: undefined,
|
||||
clearSelectionText: CLEAR_SELECTION_TEXT,
|
||||
clearSelectionText: undefined,
|
||||
};
|
||||
|
||||
ControlledSelectionStatus.propTypes = {
|
||||
/** A class name to append to the base element */
|
||||
className: PropTypes.string,
|
||||
clearSelectionText: PropTypes.string,
|
||||
/** A text that appears on the `Clear selection` button, defaults to 'Clear Selection' */
|
||||
clearSelectionText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
};
|
||||
|
||||
export default ControlledSelectionStatus;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import DataTableContext from '../DataTableContext';
|
||||
import BaseSelectionStatus from './BaseSelectionStatus';
|
||||
import { CLEAR_SELECTION_TEXT } from './data/constants';
|
||||
|
||||
const SelectionStatus = ({ className, clearSelectionText }) => {
|
||||
const { toggleAllRowsSelected, selectedFlatRows } = useContext(DataTableContext);
|
||||
|
|
@ -21,13 +20,13 @@ const SelectionStatus = ({ className, clearSelectionText }) => {
|
|||
SelectionStatus.propTypes = {
|
||||
/** A class name to append to the base element */
|
||||
className: PropTypes.string,
|
||||
/** A text that appears on the `Clear selection` button */
|
||||
clearSelectionText: PropTypes.string,
|
||||
/** A text that appears on the `Clear selection` button, defaults to 'Clear Selection' */
|
||||
clearSelectionText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
};
|
||||
|
||||
SelectionStatus.defaultProps = {
|
||||
className: undefined,
|
||||
clearSelectionText: CLEAR_SELECTION_TEXT,
|
||||
clearSelectionText: undefined,
|
||||
};
|
||||
|
||||
export default SelectionStatus;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import ControlledSelectionStatus from '../ControlledSelectionStatus';
|
||||
import { clearSelectionAction, setSelectAllRowsAllPagesAction, setSelectedRowsAction } from '../data/actions';
|
||||
|
|
@ -24,7 +25,11 @@ const instance = {
|
|||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const ControlledSelectionStatusWrapper = ({ value, props = {} }) => (
|
||||
<DataTableContext.Provider value={value}><ControlledSelectionStatus {...props} /></DataTableContext.Provider>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<DataTableContext.Provider value={value}>
|
||||
<ControlledSelectionStatus {...props} />
|
||||
</DataTableContext.Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<ControlledSelectionStatus />', () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import SelectionStatus from '../SelectionStatus';
|
||||
import DataTableContext from '../../DataTableContext';
|
||||
|
|
@ -17,7 +18,11 @@ const instance = {
|
|||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const SelectionStatusWrapper = ({ value, props = {} }) => (
|
||||
<DataTableContext.Provider value={value}><SelectionStatus {...props} /></DataTableContext.Provider>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<DataTableContext.Provider value={value}>
|
||||
<SelectionStatus {...props} />
|
||||
</DataTableContext.Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<SelectionStatus />', () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useContext } from 'react';
|
|||
import { act } from 'react-dom/test-utils';
|
||||
import { mount } from 'enzyme';
|
||||
import * as reactTable from 'react-table';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import DataTable from '..';
|
||||
import TableControlBar from '../TableControlBar';
|
||||
|
|
@ -97,43 +98,52 @@ const DataTableContextProviderChild = ({ children }) => {
|
|||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const DataTableWrapper = ({ children, ...tableProps }) => (
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<DataTable {...tableProps}>
|
||||
{children}
|
||||
</DataTable>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<DataTable />', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('displays the empty table component if empty', () => {
|
||||
const wrapper = mount(<DataTable {...props} data={[]} />);
|
||||
const wrapper = mount(<DataTableWrapper {...props} data={[]} />);
|
||||
expect(wrapper.find(EmptyTable).length).toEqual(1);
|
||||
});
|
||||
it('accepts an empty table component', () => {
|
||||
const wrapper = mount(<DataTable {...props} data={[]} EmptyTableComponent={EmptyTest} />);
|
||||
const wrapper = mount(<DataTableWrapper {...props} data={[]} EmptyTableComponent={EmptyTest} />);
|
||||
expect(wrapper.find(EmptyTable).length).toEqual(0);
|
||||
expect(wrapper.find(EmptyTest).length).toEqual(1);
|
||||
});
|
||||
it('displays a control bar', () => {
|
||||
const wrapper = mount(<DataTable {...props} />);
|
||||
const wrapper = mount(<DataTableWrapper {...props} />);
|
||||
const controlBar = wrapper.find(TableControlBar);
|
||||
expect(controlBar.length).toEqual(1);
|
||||
expect(controlBar.text()).toEqual('Showing 7 of 7.');
|
||||
});
|
||||
it('displays a table', () => {
|
||||
const wrapper = mount(<DataTable {...props} />);
|
||||
const wrapper = mount(<DataTableWrapper {...props} />);
|
||||
const table = wrapper.find(Table);
|
||||
expect(table.length).toEqual(1);
|
||||
expect(table.find('th').length).toEqual(3);
|
||||
expect(table.find('tr').length).toEqual(8);
|
||||
});
|
||||
it('displays a table footer', () => {
|
||||
const wrapper = mount(<DataTable {...props} />);
|
||||
const wrapper = mount(<DataTableWrapper {...props} />);
|
||||
expect(wrapper.find(TableFooter).length).toEqual(1);
|
||||
});
|
||||
it('adds a column when table is selectable', () => {
|
||||
const wrapper = mount(<DataTable {...props} isSelectable />);
|
||||
const wrapper = mount(<DataTableWrapper {...props} isSelectable />);
|
||||
const tableHeaders = wrapper.find(Table).find('th');
|
||||
expect(tableHeaders.length).toEqual(props.columns.length + 1);
|
||||
});
|
||||
it('adds additional columns', () => {
|
||||
const wrapper = mount(<DataTable {...props} additionalColumns={additionalColumns} />);
|
||||
const wrapper = mount(<DataTableWrapper {...props} additionalColumns={additionalColumns} />);
|
||||
const tableHeaders = wrapper.find(Table).find('th');
|
||||
expect(tableHeaders.length).toEqual(props.columns.length + additionalColumns.length);
|
||||
expect(wrapper.text()).toContain(additionalColumns[0].Header);
|
||||
|
|
@ -141,7 +151,7 @@ describe('<DataTable />', () => {
|
|||
});
|
||||
test('calls useTable with the data and columns', () => {
|
||||
const spy = jest.spyOn(reactTable, 'useTable');
|
||||
mount(<DataTable {...props} />);
|
||||
mount(<DataTableWrapper {...props} />);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy.mock.calls[0][0].columns).toEqual(props.columns);
|
||||
expect(spy.mock.calls[0][0].data).toEqual(props.data);
|
||||
|
|
@ -157,7 +167,7 @@ describe('<DataTable />', () => {
|
|||
[{ manualSortBy: true, manualFilters: true, manualPagination: true, pageCount: 1 }, { manualFilters: true, manualPagination: true, manualSortBy: true }],
|
||||
])('calls useTable with the correct manual settings %#', (additionalProps, expected) => {
|
||||
const spy = jest.spyOn(reactTable, 'useTable');
|
||||
mount(<DataTable {...props} {...additionalProps} />);
|
||||
mount(<DataTableWrapper {...props} {...additionalProps} />);
|
||||
expect(spy.mock.calls[0][0].manualFilters).toEqual(expected.manualFilters);
|
||||
expect(spy.mock.calls[0][0].manualPagination).toEqual(expected.manualPagination);
|
||||
expect(spy.mock.calls[0][0].manualSortBy).toEqual(expected.manualSortBy);
|
||||
|
|
@ -165,11 +175,11 @@ describe('<DataTable />', () => {
|
|||
it('passes the initial state to useTable', () => {
|
||||
const spy = jest.spyOn(reactTable, 'useTable');
|
||||
const initialState = { foo: 'bar' };
|
||||
mount(<DataTable {...props} initialState={initialState} />);
|
||||
mount(<DataTableWrapper {...props} initialState={initialState} />);
|
||||
expect(spy.mock.calls[0][0].initialState).toEqual(initialState);
|
||||
});
|
||||
it('displays loading state', () => {
|
||||
const wrapper = mount(<DataTable {...props} isLoading />);
|
||||
const wrapper = mount(<DataTableWrapper {...props} isLoading />);
|
||||
const tableContainer = wrapper.find('.pgn__data-table-container');
|
||||
const spinner = wrapper.find('.pgn__data-table-spinner');
|
||||
expect(tableContainer.hasClass('is-loading')).toEqual(true);
|
||||
|
|
@ -182,9 +192,9 @@ describe('<DataTable />', () => {
|
|||
describe('controlled table selections', () => {
|
||||
it('passes initial controlledTableSelections to context', () => {
|
||||
const wrapper = mount(
|
||||
<DataTable {...props}>
|
||||
<DataTableWrapper {...props}>
|
||||
<DataTableContextProviderChild />
|
||||
</DataTable>,
|
||||
</DataTableWrapper>,
|
||||
);
|
||||
const contextValue = wrapper.find('div.context-value').prop('data-contextvalue');
|
||||
const { controlledTableSelections } = contextValue;
|
||||
|
|
@ -195,7 +205,7 @@ describe('<DataTable />', () => {
|
|||
});
|
||||
it('passes appropriate selection props to context with active selections', () => {
|
||||
const wrapper = mount(
|
||||
<DataTable {...props}><DataTableContextProviderChild /></DataTable>,
|
||||
<DataTableWrapper {...props}><DataTableContextProviderChild /></DataTableWrapper>,
|
||||
);
|
||||
|
||||
// verify there are no current selections
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import ExpandAll from '../ExpandAll';
|
||||
|
||||
const ExpandAllWrapper = (props) => (
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<ExpandAll {...props} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<ExpandAll />', () => {
|
||||
it('renders expand all element if not all rows are expanded', () => {
|
||||
const wrapper = mount(<ExpandAll getToggleAllRowsExpandedProps={() => {}} isAllRowsExpanded={false} />);
|
||||
const wrapper = mount(<ExpandAllWrapper getToggleAllRowsExpandedProps={() => {}} isAllRowsExpanded={false} />);
|
||||
const labelWrapper = wrapper.find('span');
|
||||
expect(labelWrapper.exists()).toEqual(true);
|
||||
const collapseButton = wrapper.find('button');
|
||||
|
|
@ -12,7 +19,7 @@ describe('<ExpandAll />', () => {
|
|||
expect(collapseButton.text()).toContain('Expand all');
|
||||
});
|
||||
it('renders collapse all element if all rows are expanded', () => {
|
||||
const wrapper = mount(<ExpandAll getToggleAllRowsExpandedProps={() => {}} isAllRowsExpanded />);
|
||||
const wrapper = mount(<ExpandAllWrapper getToggleAllRowsExpandedProps={() => {}} isAllRowsExpanded />);
|
||||
const labelWrapper = wrapper.find('span');
|
||||
expect(labelWrapper.exists()).toEqual(true);
|
||||
const collapseButton = wrapper.find('button');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import FilterStatus from '../FilterStatus';
|
||||
import { Button } from '../..';
|
||||
|
|
@ -17,7 +18,6 @@ const filterProps = {
|
|||
className: 'filterClass',
|
||||
showFilteredFields: true,
|
||||
};
|
||||
|
||||
const filterPropsNoFiltered = {
|
||||
...filterProps,
|
||||
showFilteredFields: false,
|
||||
|
|
@ -26,7 +26,11 @@ const filterPropsNoFiltered = {
|
|||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const FilterStatusWrapper = ({ value, props }) => (
|
||||
<DataTableContext.Provider value={value}><FilterStatus {...props} /></DataTableContext.Provider>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<DataTableContext.Provider value={value}>
|
||||
<FilterStatus {...props} />
|
||||
</DataTableContext.Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<FilterStatus />', () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import RowStatus from '../RowStatus';
|
||||
import DataTableContext from '../DataTableContext';
|
||||
|
|
@ -14,7 +15,12 @@ const statusProps = {
|
|||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const RowStatusWrapper = ({ value = instance, props = statusProps }) => (
|
||||
<DataTableContext.Provider value={value}><RowStatus {...props} /></DataTableContext.Provider>);
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<DataTableContext.Provider value={value}>
|
||||
<RowStatus {...props} />
|
||||
</DataTableContext.Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<RowStatus />', () => {
|
||||
it('returns null if there is no pageSize', () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import SmartStatus from '../SmartStatus';
|
||||
import DataTableContext from '../DataTableContext';
|
||||
|
|
@ -25,7 +26,12 @@ const instance = {
|
|||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const SmartStatusWrapper = ({ value, props }) => (
|
||||
<DataTableContext.Provider value={value}><SmartStatus {...props} /></DataTableContext.Provider>);
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<DataTableContext.Provider value={value}>
|
||||
<SmartStatus {...props} />
|
||||
</DataTableContext.Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<SmartStatus />', () => {
|
||||
it('Shows the selection status if rows are selected', () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import Toast from './index';
|
||||
|
||||
/* eslint-disable-next-line react/prop-types */
|
||||
const ToastWrapper = ({ children, ...props }) => (
|
||||
<IntlProvider>
|
||||
<Toast {...props}>
|
||||
{children}
|
||||
</Toast>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<Toast />', () => {
|
||||
const onCloseHandler = () => {};
|
||||
const props = {
|
||||
|
|
@ -10,7 +20,7 @@ describe('<Toast />', () => {
|
|||
};
|
||||
it('renders optional action as link', () => {
|
||||
const wrapper = mount((
|
||||
<Toast
|
||||
<ToastWrapper
|
||||
{...props}
|
||||
action={{
|
||||
label: 'Optional action',
|
||||
|
|
@ -18,13 +28,13 @@ describe('<Toast />', () => {
|
|||
}}
|
||||
>
|
||||
Success message.
|
||||
</Toast>));
|
||||
</ToastWrapper>));
|
||||
const toastLink = wrapper.find('a.btn');
|
||||
expect(toastLink).toHaveLength(1);
|
||||
});
|
||||
it('renders optional action as button', () => {
|
||||
const wrapper = mount((
|
||||
<Toast
|
||||
<ToastWrapper
|
||||
{...props}
|
||||
action={{
|
||||
label: 'Optional action',
|
||||
|
|
@ -32,17 +42,17 @@ describe('<Toast />', () => {
|
|||
}}
|
||||
>
|
||||
Success message.
|
||||
</Toast>));
|
||||
</ToastWrapper>));
|
||||
const toastButton = wrapper.find('button.btn');
|
||||
expect(toastButton).toHaveLength(1);
|
||||
});
|
||||
it('autohide is set to false on onMouseOver and true on onMouseLeave', () => {
|
||||
const wrapper = mount((
|
||||
<Toast
|
||||
<ToastWrapper
|
||||
{...props}
|
||||
>
|
||||
Success message.
|
||||
</Toast>));
|
||||
</ToastWrapper>));
|
||||
wrapper.prop('onMouseOver');
|
||||
setTimeout(() => {
|
||||
const toast = wrapper.find(Toast);
|
||||
|
|
@ -58,11 +68,11 @@ describe('<Toast />', () => {
|
|||
});
|
||||
it('autohide is set to false onFocus and true onBlur', () => {
|
||||
const wrapper = mount((
|
||||
<Toast
|
||||
<ToastWrapper
|
||||
{...props}
|
||||
>
|
||||
Success message.
|
||||
</Toast>));
|
||||
</ToastWrapper>));
|
||||
wrapper.prop('onFocus');
|
||||
setTimeout(() => {
|
||||
const toast = wrapper.find(Toast);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import classNames from 'classnames';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import BaseToast from 'react-bootstrap/Toast';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import ToastContainer from './ToastContainer';
|
||||
import { Button, IconButton, Icon } from '..';
|
||||
import { Close } from '../../icons';
|
||||
|
|
@ -13,7 +15,13 @@ export const TOAST_DELAY = 5000;
|
|||
function Toast({
|
||||
action, children, className, closeLabel, onClose, show, ...rest
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const [autoHide, setAutoHide] = useState(true);
|
||||
const intlCloseLabel = closeLabel || intl.formatMessage({
|
||||
id: 'pgn.Toast.closeLabel',
|
||||
defaultMessage: 'Close',
|
||||
description: 'Close label for Toast component',
|
||||
});
|
||||
return (
|
||||
<ToastContainer>
|
||||
<BaseToast
|
||||
|
|
@ -34,7 +42,7 @@ function Toast({
|
|||
<div className="toast-header-btn-container">
|
||||
<IconButton
|
||||
iconAs={Icon}
|
||||
alt={closeLabel}
|
||||
alt={intlCloseLabel}
|
||||
className="align-self-start"
|
||||
src={Close}
|
||||
onClick={onClose}
|
||||
|
|
@ -61,7 +69,7 @@ function Toast({
|
|||
|
||||
Toast.defaultProps = {
|
||||
action: null,
|
||||
closeLabel: TOAST_CLOSE_LABEL_TEXT,
|
||||
closeLabel: undefined,
|
||||
delay: TOAST_DELAY,
|
||||
className: undefined,
|
||||
};
|
||||
|
|
@ -89,8 +97,7 @@ Toast.propTypes = {
|
|||
onClick: PropTypes.func,
|
||||
}),
|
||||
/**
|
||||
* Alt text for the `Toast`'s dismiss button. The recommended use is an i18n value.
|
||||
* The default is an English string.
|
||||
* Alt text for the `Toast`'s dismiss button. Defaults to 'Close'.
|
||||
*/
|
||||
closeLabel: PropTypes.string,
|
||||
/** Time in milliseconds for which the `Toast` will display. */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import arMessages from './messages/ar.json';
|
||||
import caMessages from './messages/ca.json';
|
||||
import es419Messages from './messages/es_419.json';
|
||||
import frMessages from './messages/fr.json';
|
||||
import heMessages from './messages/he.json';
|
||||
import idMessages from './messages/id.json';
|
||||
import kokrMessages from './messages/ko_KR.json';
|
||||
import plMessages from './messages/pl.json';
|
||||
import ptbrMessages from './messages/pt_BR.json';
|
||||
import ruMessages from './messages/ru.json';
|
||||
import thMessages from './messages/th.json';
|
||||
import ukMessages from './messages/uk.json';
|
||||
import zhcnMessages from './messages/zh_CN.json';
|
||||
|
||||
const messages = {
|
||||
ar: arMessages,
|
||||
ca: caMessages,
|
||||
'es-419': es419Messages,
|
||||
fr: frMessages,
|
||||
he: heMessages,
|
||||
id: idMessages,
|
||||
'ko-kr': kokrMessages,
|
||||
pl: plMessages,
|
||||
'pt-br': ptbrMessages,
|
||||
ru: ruMessages,
|
||||
th: thMessages,
|
||||
uk: ukMessages,
|
||||
'zh-cn': zhcnMessages,
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -186,3 +186,5 @@ export {
|
|||
useFlexLayout,
|
||||
} from 'react-table';
|
||||
export { default as Bubble } from './Bubble';
|
||||
|
||||
export { default as messages } from './i18n';
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
FEATURE_LANGUAGE_SWITCHER=true
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_ENABLE_AXE='true'
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ exports.onCreateWebpackConfig = ({ actions }) => {
|
|||
// in ./node_modules
|
||||
react: path.resolve(__dirname, 'node_modules/react/'),
|
||||
'react-dom': path.resolve(__dirname, 'node_modules/react-dom/'),
|
||||
'react-intl': path.resolve(__dirname, 'node_modules/react-intl/'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -30,11 +30,11 @@
|
|||
"prism-react-renderer": "^1.2.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^16.8.6",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-docgen": "^5.4.0",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-focus-on": "^3.5.4",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^5.25.0",
|
||||
"react-live": "^2.4.0",
|
||||
"rehype-autolink-headings": "^5.1.0",
|
||||
"rehype-slug": "^4.0.1",
|
||||
|
|
@ -4761,6 +4761,132 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "1.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
|
||||
"integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "0.2.25",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/@formatjs/fast-memoize": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz",
|
||||
"integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/fast-memoize/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz",
|
||||
"integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"@formatjs/icu-skeleton-parser": "1.3.6",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-messageformat-parser/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/@formatjs/icu-skeleton-parser": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz",
|
||||
"integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-skeleton-parser/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/@formatjs/intl": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.2.1.tgz",
|
||||
"integrity": "sha512-vgvyUOOrzqVaOFYzTf2d3+ToSkH2JpR7x/4U1RyoHQLmvEaTQvXJ7A2qm1Iy3brGNXC/+/7bUlc3lpH+h/LOJA==",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"@formatjs/fast-memoize": "1.2.1",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.0",
|
||||
"@formatjs/intl-displaynames": "5.4.3",
|
||||
"@formatjs/intl-listformat": "6.5.3",
|
||||
"intl-messageformat": "9.13.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^4.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-displaynames": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-5.4.3.tgz",
|
||||
"integrity": "sha512-4r12A3mS5dp5hnSaQCWBuBNfi9Amgx2dzhU4lTFfhSxgb5DOAiAbMpg6+7gpWZgl4ahsj3l2r/iHIjdmdXOE2Q==",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"@formatjs/intl-localematcher": "0.2.25",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-displaynames/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/@formatjs/intl-listformat": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-6.5.3.tgz",
|
||||
"integrity": "sha512-ozpz515F/+3CU+HnLi5DYPsLa6JoCfBggBSSg/8nOB5LYSFW9+ZgNQJxJ8tdhKYeODT+4qVHX27EeJLoxLGLNg==",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"@formatjs/intl-localematcher": "0.2.25",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-listformat/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.2.25",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
|
||||
"integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-localematcher/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/@formatjs/intl/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "0.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
|
||||
|
|
@ -5819,6 +5945,15 @@
|
|||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||
"dependencies": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-cache-semantics": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
||||
|
|
@ -8999,14 +9134,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-to-clipboard": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz",
|
||||
"integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==",
|
||||
"dependencies": {
|
||||
"toggle-selection": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.20.3",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.20.3.tgz",
|
||||
|
|
@ -15944,6 +16071,14 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"dependencies": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hosted-git-info": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||
|
|
@ -16264,6 +16399,22 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/intl-messageformat": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz",
|
||||
"integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"@formatjs/fast-memoize": "1.2.1",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.0",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/intl-messageformat/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
|
|
@ -21429,18 +21580,6 @@
|
|||
"react": "^15.3.0 || ^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-copy-to-clipboard": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
|
||||
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
|
||||
"dependencies": {
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^15.3.0 || 16 || 17 || 18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dev-utils": {
|
||||
"version": "11.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz",
|
||||
|
|
@ -21792,6 +21931,37 @@
|
|||
"react": ">=16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-intl": {
|
||||
"version": "5.25.1",
|
||||
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz",
|
||||
"integrity": "sha512-pkjdQDvpJROoXLMltkP/5mZb0/XqrqLoPGKUCfbdkP8m6U9xbK40K51Wu+a4aQqTEvEK5lHBk0fWzUV72SJ3Hg==",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.0",
|
||||
"@formatjs/intl": "2.2.1",
|
||||
"@formatjs/intl-displaynames": "5.4.3",
|
||||
"@formatjs/intl-listformat": "6.5.3",
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"@types/react": "16 || 17 || 18",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"intl-messageformat": "9.13.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0 || 17 || 18",
|
||||
"typescript": "^4.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-intl/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
|
@ -25007,11 +25177,6 @@
|
|||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toggle-selection": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
|
|
@ -30021,6 +30186,140 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@formatjs/ecma402-abstract": {
|
||||
"version": "1.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
|
||||
"integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
|
||||
"requires": {
|
||||
"@formatjs/intl-localematcher": "0.2.25",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@formatjs/fast-memoize": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz",
|
||||
"integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==",
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@formatjs/icu-messageformat-parser": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz",
|
||||
"integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"@formatjs/icu-skeleton-parser": "1.3.6",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@formatjs/icu-skeleton-parser": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz",
|
||||
"integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@formatjs/intl": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.2.1.tgz",
|
||||
"integrity": "sha512-vgvyUOOrzqVaOFYzTf2d3+ToSkH2JpR7x/4U1RyoHQLmvEaTQvXJ7A2qm1Iy3brGNXC/+/7bUlc3lpH+h/LOJA==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"@formatjs/fast-memoize": "1.2.1",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.0",
|
||||
"@formatjs/intl-displaynames": "5.4.3",
|
||||
"@formatjs/intl-listformat": "6.5.3",
|
||||
"intl-messageformat": "9.13.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@formatjs/intl-displaynames": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-5.4.3.tgz",
|
||||
"integrity": "sha512-4r12A3mS5dp5hnSaQCWBuBNfi9Amgx2dzhU4lTFfhSxgb5DOAiAbMpg6+7gpWZgl4ahsj3l2r/iHIjdmdXOE2Q==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"@formatjs/intl-localematcher": "0.2.25",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@formatjs/intl-listformat": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-6.5.3.tgz",
|
||||
"integrity": "sha512-ozpz515F/+3CU+HnLi5DYPsLa6JoCfBggBSSg/8nOB5LYSFW9+ZgNQJxJ8tdhKYeODT+4qVHX27EeJLoxLGLNg==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"@formatjs/intl-localematcher": "0.2.25",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@formatjs/intl-localematcher": {
|
||||
"version": "0.2.25",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
|
||||
"integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "0.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
|
||||
|
|
@ -30873,6 +31172,15 @@
|
|||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"@types/hoist-non-react-statics": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||
"requires": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"@types/http-cache-semantics": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
||||
|
|
@ -33380,14 +33688,6 @@
|
|||
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
|
||||
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
|
||||
},
|
||||
"copy-to-clipboard": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz",
|
||||
"integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==",
|
||||
"requires": {
|
||||
"toggle-selection": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"core-js": {
|
||||
"version": "3.20.3",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.20.3.tgz",
|
||||
|
|
@ -38657,6 +38957,14 @@
|
|||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="
|
||||
},
|
||||
"hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"requires": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||
|
|
@ -38886,6 +39194,24 @@
|
|||
"side-channel": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"intl-messageformat": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz",
|
||||
"integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"@formatjs/fast-memoize": "1.2.1",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
|
|
@ -42577,15 +42903,6 @@
|
|||
"@babel/runtime": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"react-copy-to-clipboard": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
|
||||
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
|
||||
"requires": {
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"prop-types": "^15.8.1"
|
||||
}
|
||||
},
|
||||
"react-dev-utils": {
|
||||
"version": "11.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz",
|
||||
|
|
@ -42851,6 +43168,30 @@
|
|||
"react-side-effect": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"react-intl": {
|
||||
"version": "5.25.1",
|
||||
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz",
|
||||
"integrity": "sha512-pkjdQDvpJROoXLMltkP/5mZb0/XqrqLoPGKUCfbdkP8m6U9xbK40K51Wu+a4aQqTEvEK5lHBk0fWzUV72SJ3Hg==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.4",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.0",
|
||||
"@formatjs/intl": "2.2.1",
|
||||
"@formatjs/intl-displaynames": "5.4.3",
|
||||
"@formatjs/intl-listformat": "6.5.3",
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"@types/react": "16 || 17 || 18",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"intl-messageformat": "9.13.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
|
@ -45311,11 +45652,6 @@
|
|||
"is-number": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"toggle-selection": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
|
||||
},
|
||||
"toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
"description": "Paragon Pattern Library Documentation",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@edx/brand-edx.org": "^2.0.6",
|
||||
"@docsearch/react": "^3.0.0",
|
||||
"@edx/brand-edx.org": "^2.0.6",
|
||||
"@edx/brand-openedx": "^1.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@mdx-js/mdx": "^1.6.22",
|
||||
|
|
@ -29,6 +29,7 @@
|
|||
"react-focus-on": "^3.5.4",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-live": "^2.4.0",
|
||||
"react-intl": "^5.25.0",
|
||||
"rehype-autolink-headings": "^5.1.0",
|
||||
"rehype-slug": "^4.0.1",
|
||||
"sass": "^1.32.13",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import theme from 'prism-react-renderer/themes/duotoneDark';
|
|||
import {
|
||||
LiveProvider, LiveEditor, LiveError, LivePreview,
|
||||
} from 'react-live';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import * as ParagonReact from '~paragon-react'; // eslint-disable-line
|
||||
import * as ParagonIcons from '~paragon-icons'; // eslint-disable-line
|
||||
import MiyazakiCard from './exampleComponents/MiyazakiCard';
|
||||
|
|
@ -41,7 +42,12 @@ CollapsibleLiveEditor.propTypes = {
|
|||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
function CodeBlock({ children, className, live }) {
|
||||
function CodeBlock({
|
||||
children,
|
||||
className,
|
||||
live,
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const language = className ? className.replace(/language-/, '') : 'jsx';
|
||||
|
||||
if (live) {
|
||||
|
|
@ -58,6 +64,8 @@ function CodeBlock({ children, className, live }) {
|
|||
useMemo,
|
||||
MiyazakiCard,
|
||||
HipsterIpsum,
|
||||
FormattedMessage,
|
||||
formatMessage: intl.formatMessage,
|
||||
MenuIcon: ParagonIcons.Menu,
|
||||
}}
|
||||
theme={theme}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { MDXRenderer } from 'gatsby-plugin-mdx';
|
|||
import PropType from './PropType';
|
||||
import { Badge, Card } from '~paragon-react'; // eslint-disable-line
|
||||
|
||||
const IGNORED_COMPONENT_PROPS = ['intl'];
|
||||
|
||||
const DefaultValue = ({ value }) => {
|
||||
if (!value || value === 'undefined') { return null; }
|
||||
return (
|
||||
|
|
@ -69,7 +71,9 @@ const PropsTable = ({ props: componentProps, displayName, content }) => (
|
|||
{content && <div className="small mb-3">{content}</div>}
|
||||
{componentProps.length > 0 ? (
|
||||
<ul className="list-unstyled">
|
||||
{componentProps.map(metadata => <Prop key={metadata.name} {...metadata} />)}
|
||||
{componentProps
|
||||
.filter(metadata => !IGNORED_COMPONENT_PROPS.includes(metadata.name))
|
||||
.map(metadata => <Prop key={metadata.name} {...metadata} />)}
|
||||
</ul>
|
||||
) : <div className="pb-3 pl-4">This component does not receive any props.</div>}
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -4,20 +4,20 @@ import {
|
|||
Form,
|
||||
Icon,
|
||||
IconButton,
|
||||
Stack,
|
||||
} from '~paragon-react';
|
||||
import { Close } from '~paragon-icons';
|
||||
|
||||
import { FEATURES, LANGUAGES } from '../config';
|
||||
import SettingsContext from '../context/SettingsContext';
|
||||
import { THEMES } from '../../theme-config';
|
||||
|
||||
const Settings = () => {
|
||||
const {
|
||||
theme: currentTheme,
|
||||
onThemeChange,
|
||||
settings,
|
||||
handleSettingsChange,
|
||||
showSettings,
|
||||
closeSettings,
|
||||
direction,
|
||||
onDirectionChange,
|
||||
} = useContext(SettingsContext);
|
||||
|
||||
return (
|
||||
|
|
@ -38,12 +38,12 @@ const Settings = () => {
|
|||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="pgn__settings-form-wrapper">
|
||||
<Stack gap={3}>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="select"
|
||||
value={currentTheme}
|
||||
onChange={onThemeChange}
|
||||
value={settings.theme}
|
||||
onChange={(e) => handleSettingsChange('theme', e.target.value)}
|
||||
floatingLabel="Theme"
|
||||
>
|
||||
{THEMES.map(theme => (
|
||||
|
|
@ -59,15 +59,34 @@ const Settings = () => {
|
|||
<Form.Group>
|
||||
<Form.Control
|
||||
as="select"
|
||||
value={direction}
|
||||
onChange={onDirectionChange}
|
||||
value={settings.direction}
|
||||
onChange={(e) => handleSettingsChange('direction', e.target.value)}
|
||||
floatingLabel="Direction"
|
||||
>
|
||||
<option value="ltr">Left to right</option>
|
||||
<option value="rtl">Right to left</option>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</div>
|
||||
{FEATURES.LANGUAGE_SWITCHER && (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="select"
|
||||
value={settings.language}
|
||||
onChange={(e) => handleSettingsChange('language', e.target.value)}
|
||||
floatingLabel="Component Language"
|
||||
>
|
||||
{LANGUAGES.map(lang => (
|
||||
<option
|
||||
key={lang.code}
|
||||
value={lang.code}
|
||||
>
|
||||
{lang.label}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
)}
|
||||
</Stack>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// import hasFeatureFlagEnabled from './utils/hasFeatureFlagEnabled';
|
||||
const hasFeatureFlagEnabled = require('./utils/hasFeatureFlagEnabled');
|
||||
|
||||
// export const EXAMPLE_FEATURE = 'EXAMPLE_FEATURE';
|
||||
const FEATURE_LANGUAGE_SWITCHER = 'LANGUAGE_SWITCHER';
|
||||
|
||||
// Feature flags used throughout the site should be configured here.
|
||||
// You should generally allow two ways of enabling a feature flag:
|
||||
|
|
@ -11,12 +11,70 @@
|
|||
// See DIRECTION_SWITCHER feature for example of configuring feature flags this way.
|
||||
// 2. As a query parameter in the URL, using hasFeatureFlagEnabled util function.
|
||||
// This will allow to enable feature flag by providing its name as a feature?
|
||||
// query parameter in the URL. (e.g. to enable DIRECTION_SWITCHER feature you would append
|
||||
// '?feature=DIRECTION_SWITCHER' to the URL)
|
||||
// query parameter in the URL. (e.g. to enable LANGUAGE_SWITCHER feature you would append
|
||||
// '?feature=LANGUAGE_SWITCHER' to the URL)
|
||||
const FEATURES = {
|
||||
LANGUAGE_SWITCHER: process.env.FEATURE_LANGUAGE_SWITCHER || hasFeatureFlagEnabled(FEATURE_LANGUAGE_SWITCHER),
|
||||
};
|
||||
|
||||
// export const FEATURES = {
|
||||
// EXAMPLE_FEATURE: process.env.EXAMPLE_FEATURE || hasFeatureFlagEnabled(EXAMPLE_FEATURE),
|
||||
// };
|
||||
const LANGUAGES = [
|
||||
{
|
||||
label: 'English',
|
||||
code: 'en',
|
||||
},
|
||||
{
|
||||
label: 'Arabic',
|
||||
code: 'ar',
|
||||
},
|
||||
{
|
||||
label: 'Catalan',
|
||||
code: 'ca',
|
||||
},
|
||||
{
|
||||
label: 'Chinese',
|
||||
code: 'zh-cn',
|
||||
},
|
||||
{
|
||||
label: 'French',
|
||||
code: 'fr',
|
||||
},
|
||||
{
|
||||
label: 'Hebrew',
|
||||
code: 'he',
|
||||
},
|
||||
{
|
||||
label: 'Indonesian',
|
||||
code: 'id',
|
||||
},
|
||||
{
|
||||
label: 'Polish',
|
||||
code: 'pl',
|
||||
},
|
||||
{
|
||||
label: 'Russian',
|
||||
code: 'ru',
|
||||
},
|
||||
{
|
||||
label: 'Thai',
|
||||
code: 'th',
|
||||
},
|
||||
{
|
||||
label: 'Ukrainian',
|
||||
code: 'uk',
|
||||
},
|
||||
{
|
||||
label: 'Spanish',
|
||||
code: 'es-419',
|
||||
},
|
||||
{
|
||||
label: 'Korean',
|
||||
code: 'ko-kr',
|
||||
},
|
||||
{
|
||||
label: 'Portuguese',
|
||||
code: 'pt-br',
|
||||
},
|
||||
];
|
||||
|
||||
const INSIGHTS_TABS = Object.freeze({
|
||||
SUMMARY: 'Summary',
|
||||
|
|
@ -42,4 +100,7 @@ const INSIGHTS_PAGES = [
|
|||
module.exports = {
|
||||
INSIGHTS_TABS,
|
||||
INSIGHTS_PAGES,
|
||||
FEATURES,
|
||||
LANGUAGES,
|
||||
FEATURE_LANGUAGE_SWITCHER,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import React, { createContext, useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { messages } from '~paragon-react';
|
||||
|
||||
import { THEMES } from '../../theme-config';
|
||||
|
||||
const defaultValue = {
|
||||
theme: 'openedx-theme',
|
||||
onThemeChange: () => {},
|
||||
direction: 'ltr',
|
||||
onDirectionChange: () => {},
|
||||
settings: {},
|
||||
handleSettingsChange: () => {},
|
||||
};
|
||||
|
||||
export const SettingsContext = createContext(defaultValue);
|
||||
|
|
@ -15,38 +16,33 @@ export const SettingsContext = createContext(defaultValue);
|
|||
const SettingsContextProvider = ({ children }) => {
|
||||
// gatsby does not have access to the localStorage during the build (and first render)
|
||||
// so sadly we cannot initialize theme with value from localStorage
|
||||
const [theme, setTheme] = useState('openedx-theme');
|
||||
const [settings, setSettings] = useState({
|
||||
theme: 'openedx-theme',
|
||||
direction: 'ltr',
|
||||
language: 'en',
|
||||
});
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [direction, setDirection] = useState('ltr');
|
||||
|
||||
const handleDirectionChange = (e) => {
|
||||
document.body.setAttribute('dir', e.target.value);
|
||||
setDirection(e.target.value);
|
||||
global.localStorage.setItem('pgn__direction', e.target.value);
|
||||
global.analytics.track('Direction change', { direction: e.target.value });
|
||||
const handleSettingsChange = (key, value) => {
|
||||
if (key === 'direction') {
|
||||
document.body.setAttribute('dir', value);
|
||||
}
|
||||
setSettings(prevState => ({ ...prevState, [key]: value }));
|
||||
global.localStorage.setItem('pgn__settings', JSON.stringify({ ...settings, [key]: value }));
|
||||
global.analytics.track(`${key[0].toUpperCase() + key.slice(1)} change`, { [key]: value });
|
||||
};
|
||||
|
||||
const handleThemeChange = (e) => {
|
||||
setTheme(e.target.value);
|
||||
global.localStorage.setItem('pgn__theme', e.target.value);
|
||||
global.analytics.track('Theme change', { theme: e.target.value });
|
||||
};
|
||||
|
||||
const handleSettingsChange = (value) => {
|
||||
const toggleSettings = (value) => {
|
||||
setShowSettings(value);
|
||||
global.analytics.track('Toggle Settings', { value: value ? 'show' : 'hide' });
|
||||
};
|
||||
|
||||
// this hook will be called after the first render, so we can safely access localStorage
|
||||
useEffect(() => {
|
||||
const savedTheme = global.localStorage.getItem('pgn__theme');
|
||||
if (savedTheme) {
|
||||
setTheme(savedTheme);
|
||||
}
|
||||
const savedDirection = global.localStorage.getItem('pgn__direction');
|
||||
if (savedDirection) {
|
||||
document.body.setAttribute('dir', savedDirection);
|
||||
setDirection(savedDirection);
|
||||
const savedSettings = JSON.parse(global.localStorage.getItem('pgn__settings'));
|
||||
if (savedSettings) {
|
||||
setSettings(savedSettings);
|
||||
document.body.setAttribute('dir', savedSettings.direction);
|
||||
}
|
||||
if (!global.analytics) {
|
||||
global.analytics = {};
|
||||
|
|
@ -55,13 +51,11 @@ const SettingsContextProvider = ({ children }) => {
|
|||
}, []);
|
||||
|
||||
const contextValue = {
|
||||
theme,
|
||||
direction,
|
||||
settings,
|
||||
showSettings,
|
||||
onThemeChange: handleThemeChange,
|
||||
onDirectionChange: handleDirectionChange,
|
||||
closeSettings: () => handleSettingsChange(false),
|
||||
openSettings: () => handleSettingsChange(true),
|
||||
handleSettingsChange,
|
||||
closeSettings: () => toggleSettings(false),
|
||||
openSettings: () => toggleSettings(true),
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -77,12 +71,14 @@ const SettingsContextProvider = ({ children }) => {
|
|||
<link
|
||||
key={themeInfo.stylesheet}
|
||||
href={`/static/${themeInfo.stylesheet}.css`}
|
||||
rel={`stylesheet${theme === themeInfo.stylesheet ? '' : ' alternate'}`}
|
||||
rel={`stylesheet${settings.theme === themeInfo.stylesheet ? '' : ' alternate'}`}
|
||||
type="text/css"
|
||||
/>
|
||||
))}
|
||||
</Helmet>
|
||||
{children}
|
||||
<IntlProvider messages={messages[settings.language]} locale={settings.language.split('-')[0]}>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* @param {string} featureFlag
|
||||
* @returns true if feature flag is in `?feature` query parameter
|
||||
*/
|
||||
export default function hasFeatureFlagEnabled(featureFlag) {
|
||||
function hasFeatureFlagEnabled(featureFlag) {
|
||||
const { location } = global;
|
||||
if (!location) {
|
||||
return false;
|
||||
|
|
@ -13,3 +13,5 @@ export default function hasFeatureFlagEnabled(featureFlag) {
|
|||
const features = searchParams.getAll('feature');
|
||||
return features.includes(featureFlag);
|
||||
}
|
||||
|
||||
module.exports = hasFeatureFlagEnabled;
|
||||
|
|
|
|||
Loading…
Reference in New Issue