diff --git a/example/app.tsx b/example/app.tsx index 1fdf0ba..c6f39ef 100644 --- a/example/app.tsx +++ b/example/app.tsx @@ -12,7 +12,7 @@ const navItems = [ { title: 'Overview', to: '/', - exact: true + exact: true, }, { title: 'Getting Started', @@ -20,7 +20,7 @@ const navItems = [ items: [ { to: '/getting-started/installation', title: 'Installation' }, { to: '/getting-started/usage', title: 'Usage' }, - ] + ], }, { title: 'API', @@ -38,11 +38,13 @@ const navItems = [ { to: '/api/useBreadcrumb', title: 'useBreadcrumb' }, { to: '/api/useDocumentTitle', title: 'useDocumentTitle' }, ], - }, { + }, + { title: 'Examples', to: '/examples', items: [ - { to: '/examples/async-data-list', title: 'Async Data List' }, + { to: '/examples/async-data-list-rest', title: 'Async Data List (REST)' }, + { to: '/examples/async-data-list-graphql', title: 'Async Data List (Graphql)' }, ], }, ]; @@ -50,13 +52,17 @@ const navItems = [ const getOverviewPage = () => import('./pages/OverviewPage'); const getInstallationPage = () => import('./pages/InstallationPage'); const getUsagePage = () => import('./pages/UsagePage'); -const getAsyncDataList = () => import('./pages/AsyncDataList'); +const getAsyncDataListRest = () => import('./pages/AsyncDataListRestPage'); +const getAsyncDataListGraphQL = () => import('./pages/AsyncDataListGraphQLPage'); export const App = () => { const history = useHistory(); - const logoProps = React.useMemo(() => ({ - onClick: () => history.push('/') - }), [history]); + const logoProps = React.useMemo( + () => ({ + onClick: () => history.push('/'), + }), + [history] + ); return ( { navGroupsStyle={'expandable'} > - + + + - ); -}; \ No newline at end of file +}; diff --git a/example/components/AsyncDataListGraphQLHeader.tsx b/example/components/AsyncDataListGraphQLHeader.tsx new file mode 100644 index 0000000..b410994 --- /dev/null +++ b/example/components/AsyncDataListGraphQLHeader.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { PageSection, Text, TextContent, Title } from '@patternfly/react-core'; +import { ExampleLink } from './ExampleLink'; + +export const AsyncDataListGraphQLHeader: React.FunctionComponent = () => ( + + + + Async Data List (GraphQL) + + + A common pattern is to query a GraphQL endpoint, wait for its results and show + the data in a data list. In this example we use SWApi GraphQL as a sample backend service + to show the list of characters in the Star Wars universe. + + + + Source for this example + + . + + + +); diff --git a/example/components/AsyncDataListRestHeader.tsx b/example/components/AsyncDataListRestHeader.tsx new file mode 100644 index 0000000..61b1ebc --- /dev/null +++ b/example/components/AsyncDataListRestHeader.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { PageSection, Text, TextContent, Title } from '@patternfly/react-core'; +import { ExampleLink } from './ExampleLink'; + +export const AsyncDataListRestHeader: React.FunctionComponent = () => ( + + + + Async Data List (REST) + + + A common pattern is to call a REST API, wait for its results and show + the data in a data list. In this example we use {' '} + SWApi as a sample backend service + to show the list of characters in the Star Wars universe. + + + + Source for this example + + . + + + +); diff --git a/example/components/ExampleLink.tsx b/example/components/ExampleLink.tsx index 1bdfed2..148c18b 100644 --- a/example/components/ExampleLink.tsx +++ b/example/components/ExampleLink.tsx @@ -1,8 +1,16 @@ import * as React from 'react'; -export interface IExampleLinkProps extends React.HTMLAttributes { +export interface IExampleLinkProps + extends React.HTMLAttributes { filename: string; } -export const ExampleLink: React.FunctionComponent = ({ filename, ...props }) => - \ No newline at end of file +export const ExampleLink: React.FunctionComponent = ({ + filename, + ...props +}) => ( + +); diff --git a/example/components/GettingStarted.tsx b/example/components/GettingStarted.tsx index d207926..a5e39d0 100644 --- a/example/components/GettingStarted.tsx +++ b/example/components/GettingStarted.tsx @@ -1,18 +1,28 @@ import * as React from 'react'; -import { PageSection, Title, EmptyState, EmptyStateVariant, EmptyStateIcon, EmptyStateBody, Button, EmptyStateSecondaryActions } from '@patternfly/react-core'; +import { + PageSection, + Title, + EmptyState, + EmptyStateVariant, + EmptyStateIcon, + EmptyStateBody, + Button, + EmptyStateSecondaryActions, +} from '@patternfly/react-core'; import { CubesIcon } from '@patternfly/react-icons'; -export const GettingStarted: React.FunctionComponent = ({ children, ...props }) => { +export const GettingStarted: React.FunctionComponent = ({ children }) => { return ( - + Empty State (Stub Support Module) - This represents an the empty state pattern in Patternfly 4. Hopefully it's simple enough to use but flexible - enough to meet a variety of needs. + This represents an the empty state pattern in Patternfly 4. Hopefully + it's simple enough to use but flexible enough to meet a variety of + needs. @@ -26,4 +36,4 @@ export const GettingStarted: React.FunctionComponent = ({ children, ...prop ); -} +}; diff --git a/example/components/Installation.tsx b/example/components/Installation.tsx index a04c538..898602d 100644 --- a/example/components/Installation.tsx +++ b/example/components/Installation.tsx @@ -13,43 +13,56 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; import { Keyword } from './Keyword'; - -export const Installation: React.FunctionComponent = ({ children, ...props }) => { +export const Installation: React.FunctionComponent = ({ children }) => { return ( - + Installation - You can install use-patternfly from npm: - - npm install use-patternfly --save + + You can install use-patternfly from npm: + npm install use-patternfly --save Or if you're using Yarn: - - yarn add use-patternfly - + yarn add use-patternfly This package requires the following packages as peer dependencies: - @patternfly/react-core - @patternfly/react-styles - @patternfly/react-icons - react-router - react-router-dom + + @patternfly/react-core + + + @patternfly/react-styles + + + @patternfly/react-icons + + + react-router + + + react-router-dom + - Please make sure to install them as well. You'll need react@16.8.0 + Please make sure to install them as well. You'll need{' '} + react@16.8.0 or later since the package uses hooks. - Some of the provided components and hooks could require additional dependencies. + Some of the provided components and hooks could require additional + dependencies. - - + + + + ); -} +}; diff --git a/example/components/Keyword.tsx b/example/components/Keyword.tsx index add5210..1cefefa 100644 --- a/example/components/Keyword.tsx +++ b/example/components/Keyword.tsx @@ -7,12 +7,10 @@ const styles = StyleSheet.create({ backgroundColor: `var(--pf-global--BackgroundColor--light-200)`, fontFamily: 'monospace', padding: '3px', - borderRadius: '3px' - } + borderRadius: '3px', + }, }); -export const Keyword: React.FunctionComponent = props => - ; +export const Keyword: React.FunctionComponent = props => ( + +); diff --git a/example/components/Overview.tsx b/example/components/Overview.tsx index 7cc7415..23fe427 100644 --- a/example/components/Overview.tsx +++ b/example/components/Overview.tsx @@ -1,59 +1,100 @@ import * as React from 'react'; -import { PageSection, TextContent, Title, Text, FlexItem, Flex, Button } from '@patternfly/react-core'; +import { + PageSection, + TextContent, + Title, + Text, + FlexItem, + Flex, + Button, +} from '@patternfly/react-core'; import { Link } from 'react-router-dom'; import logo from '../use-patternfly.png'; import { Keyword } from './Keyword'; -export const Overview: React.FunctionComponent = ({ children, ...props }) => { +export const Overview: React.FunctionComponent = ({ children }) => { return ( <> - -
- {'use-patternfly -
+ +
+ {'use-patternfly +
+
+ {'Build + +   + + {'Coverage + + + + + Overview + + use-patternfly is an opinionated set of hooks and + components useful when building a React app with{' '} + PatternFly. + + + This project aims to encourage code reuse between all projects using + PatternFly and to showcase how to implement features like fetching + from an API and showing the data in a DataList or Table component. + - - {'Build -   - - {'Coverage - - - - - Overview - - use-patternfly is an opinionated set of hooks and components useful when building a React app with PatternFly. - - - This project aims to encourage code reuse between all projects using PatternFly and to showcase how to implement features like fetching from an API and showing the data in a DataList or Table component. - + + Motivation + + + Since I ( + @riccardoforina) + started building apps using PatternFly, I found myself writing + solutions to the same problem many times. But how many ways can + there be to{' '} + set the document title or{' '} + + wrap an app in a PatternFly chrome + + ? I felt the need to finalize these solutions in something reusable + and well-tested, to finally be able to focus on the app needs + instead of re-learning how to wire PatternFly's components once + again. + - Motivation - - Since I (@riccardoforina) started - building apps using PatternFly, I found myself writing solutions to the same problem - many times. But how many ways can there be to set the document title or wrap an app in a PatternFly chrome? I felt the need - to finalize these solutions in something reusable and well-tested, to finally - be able to focus on the app needs instead of re-learning how to wire PatternFly's - components once again. - + + Dependencies + + + The only real dependency-other than PatternFly itself-is{' '} + + react-router + + , although it's listed as a peer dependency to avoid clashing with + whatever version you are running on your project. Other utilities + might have extra dependencies, but if you don't plan on using that + specific utility you don't need to install them. + + - Dependencies - The only real dependency-other than PatternFly itself-is react-router, - although it's listed as a peer dependency to avoid clashing with whatever - version you are running on your project. Other utilities might have extra - dependencies, but if you don't plan on using that specific utility you - don't need to install them. - - - - - - - - - - + + + + + + + + + ); -} +}; diff --git a/example/components/StarWarsPeople.tsx b/example/components/StarWarsPeople.tsx index 5a2e3fd..23262d5 100644 --- a/example/components/StarWarsPeople.tsx +++ b/example/components/StarWarsPeople.tsx @@ -5,31 +5,33 @@ import { DataListItem, DataListItemCells, DataListItemRow, - PageSection, Pagination, Text, - TextContent, TextList, TextListItem, TextListItemVariants, TextListVariants, + Flex, + FlexItem, + PageSection, + Pagination, + Text, + TextContent, Title, + Tooltip, + TooltipPosition, } from '@patternfly/react-core'; -import { InfoIcon } from '@patternfly/react-icons'; -import { Loading } from 'use-patternfly'; -import { ExampleLink } from './ExampleLink'; +import { + InfoIcon, + VirtualMachineIcon, + VideoIcon, + SpaceShuttleIcon, +} from '@patternfly/react-icons'; +import { Loading, FormatRelative } from 'use-patternfly'; +import '@patternfly/react-styles/css/components/Toolbar/toolbar.css'; export interface IStarWarsPerson { name: string; - height: string; mass: string; - hair_color: string; - skin_color: string; - eye_color: string; - birth_year: string; gender: string; - homeworld: string; - films: string[]; - species: string[]; - vehicles: string[]; - starships: string[]; - created: Date; - edited: Date; - url: string; + films: number; + vehicles: number; + starships: number; + updatedAt: Date; } export interface IStarWarsPeopleProps { @@ -38,7 +40,8 @@ export interface IStarWarsPeopleProps { page: number; total: number; loading: boolean; - onPageChange: (page: number) => void + onPageChange: (page: number) => void; + onPerPageChange: (page: number) => void; } export const StarWarsPeople: React.FunctionComponent = ({ @@ -48,38 +51,25 @@ export const StarWarsPeople: React.FunctionComponent = ({ total, loading, onPageChange, - ...props + onPerPageChange }) => { - const onPerPageSelect = () => false; const paginationProps = { itemCount: total, page: page, perPage: perPage, onSetPage: (_, page) => onPageChange(page), - onPerPageSelect: onPerPageSelect, - perPageOptions: [{ title: `Sorry, SWApi doesn't support this`, value: perPage }], + onPerPageSelect: (_, page) => onPerPageChange(page) }; return ( - <> - - - Async Data List - - A common pattern is to call an API, wait for its results and show - the data in a data list. In this example we use the - SWApi as a sample backend service - to show the list of characters in the Star Wars universe. - - Source for this example. - - - - - - {loading ? : people.map(person => { + +
+ +
+ + {loading ? ( + + ) : ( + people.map(person => { const itemId = `sw-person-${person.name}`; return ( @@ -91,52 +81,60 @@ export const StarWarsPeople: React.FunctionComponent = ({ , - {person.name} - Gender: {person.gender} Mass: {person.mass} - - , - - - - Films - - {person.films.length} - - - - , - - - - Vehicles - - {person.vehicles.length} - - + + {person.name} + + + Gender: {person.gender} Mass: {person.mass} + + + + Films} + > + + +   + {person.films} + + + Vehicles} + > + + +   + {person.vehicles} + + + Starships} + > + + +   + {person.starships} + + + Updated + + , - - - - Starships - - {person.starships.length} - - - - ]} /> ); - })} - - -
- + }) + )} +
+
+ +
+
); -} +}; diff --git a/example/components/Usage.tsx b/example/components/Usage.tsx index 14a5abc..1e161fd 100644 --- a/example/components/Usage.tsx +++ b/example/components/Usage.tsx @@ -10,32 +10,57 @@ import { import * as React from 'react'; import { Link } from 'react-router-dom'; -export const Usage: React.FunctionComponent = ({ children, ...props }) => { +export const Usage: React.FunctionComponent = ({ children }) => { return ( - + Usage - Import the component or hook that you need using the object destructuring syntax: + + Import the component or hook that you need using the object + destructuring syntax: + - - Another source of usage examples is the code behind this website. + + Another source of usage examples is{' '} + + the code behind this website + + . + - - + + + + ); -} +}; diff --git a/example/components/index.ts b/example/components/index.ts index dba9ebe..41760aa 100644 --- a/example/components/index.ts +++ b/example/components/index.ts @@ -1,3 +1,5 @@ +export * from './AsyncDataListGraphQLHeader'; +export * from './AsyncDataListRestHeader'; export * from './ExampleLink'; export * from './GettingStarted'; export * from './Installation'; diff --git a/example/index.tsx b/example/index.tsx index 43ed48a..8377131 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -4,12 +4,11 @@ import { BrowserRouter as Router } from 'react-router-dom'; import { LastLocationProvider } from 'react-router-last-location'; import { App } from './app'; -ReactDOM.render(( - - - - - - ), - document.getElementById("root") as HTMLElement +ReactDOM.render( + + + + + , + document.getElementById('root') as HTMLElement ); diff --git a/example/package.json b/example/package.json index ccfc09c..7205051 100644 --- a/example/package.json +++ b/example/package.json @@ -8,6 +8,9 @@ "build": "parcel build index.html" }, "dependencies": { + "@apollo/react-hooks": "^3.1.2", + "apollo-boost": "^0.4.4", + "graphql": "^14.5.8", "react-app-polyfill": "^1.0.4", "react-async": "^9.0.0" }, @@ -18,7 +21,8 @@ "react-router": "../node_modules/react-router", "react-router-dom": "../node_modules/react-router-dom", "react-router-last-location": "../node_modules/react-router-last-location", - "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" + "scheduler/tracing": "../node_modules/scheduler/tracing-profiling", + "date-fns": "../node_modules/date-fns" }, "devDependencies": { "@types/react": "^16.8.15", diff --git a/example/pages/AsyncDataList.tsx b/example/pages/AsyncDataList.tsx deleted file mode 100644 index 60daf20..0000000 --- a/example/pages/AsyncDataList.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from 'react'; -import { useFetch } from 'react-async'; -import { useHistory, useLocation } from 'react-router-dom'; -import { useA11yRouteContainer, useDocumentTitle } from 'use-patternfly'; -import { IStarWarsPerson, StarWarsPeople } from '../components'; - -interface IStarWarsPeopleResponse { - count: number; - next: string; - previous?: string; - results: IStarWarsPerson[] -} - -export default function AsyncDataList() { - useDocumentTitle('Async Data List'); - const a11yContainerProps = useA11yRouteContainer(); - const location = useLocation(); - const history = useHistory(); - const searchParams = new URLSearchParams(location.search); - const page = parseInt(searchParams.get('page') || '', 10) || 1; - const { data, isPending } = useFetch( - `https://swapi.co/api/people/?page=${page}`, - { headers: { Accept: 'application/json' } } - ); - const handlePageChange = React.useCallback((newPage: number) => { - searchParams.set('page', newPage.toString()); - history.push({ - search: searchParams.toString() - }) - }, [searchParams, history]); - return ( - - ); -} diff --git a/example/pages/AsyncDataListGraphQLPage.tsx b/example/pages/AsyncDataListGraphQLPage.tsx new file mode 100644 index 0000000..018e70d --- /dev/null +++ b/example/pages/AsyncDataListGraphQLPage.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import ApolloClient, { gql } from 'apollo-boost'; +import { ApolloProvider, useQuery } from '@apollo/react-hooks'; +import { useA11yRouteChange, useDocumentTitle } from 'use-patternfly'; +import { AsyncDataListGraphQLHeader, IStarWarsPerson, StarWarsPeople } from '../components'; + +const client = new ApolloClient({ + uri: 'https://swapi.graph.cool/', +}); + +const SW_PEOPLE = gql` +query People($first: Int, $skip: Int) { + allPersons(first: $first, skip: $skip) { + name + gender + mass + _filmsMeta { + count + } + _vehiclesMeta { + count + } + _starshipsMeta { + count + } + updatedAt + } + _allPersonsMeta { + count + } +} +`; + +interface IStarWarsPeopleResponse { + count: number; + next: string; + previous?: string; + results: IStarWarsPerson[]; +} + +function AsyncDataListGraphQL() { + useDocumentTitle('Async Data List (GraphQL)'); + useA11yRouteChange(); + const location = useLocation(); + const history = useHistory(); + const searchParams = new URLSearchParams(location.search); + const page = parseInt(searchParams.get('page') || '', 10) || 0; + const perPage = parseInt(searchParams.get('perPage') || '', 10) || 10; + + const { loading, data } = useQuery(SW_PEOPLE, { + variables: { + first: perPage, + skip: page * perPage + } + }); + + const setSearchParam = React.useCallback((name: string, value: string) => { + searchParams.set(name, value.toString()); + }, [searchParams]); + + const handlePageChange = React.useCallback( + (newPage: number) => { + setSearchParam('page', (newPage - 1).toString()); + history.push({ + search: searchParams.toString(), + }); + }, + [setSearchParam, history] + ); + + const handlePerPageChange = React.useCallback( + (newPerPage: number) => { + setSearchParam('page', '0'); + setSearchParam('perPage', newPerPage.toString()); + history.push({ + search: searchParams.toString(), + }); + }, + [setSearchParam, history] + ); + + const { allPersons, _allPersonsMeta: { count: total } } = data || { allPersons: [], _allPersonsMeta: { count: 0 } }; + const people = allPersons.map(person => ({ + name: person.name, + mass: person.mass, + gender: person.gender, + films: person._filmsMeta.count, + vehicles: person._vehiclesMeta.count, + starships: person._starshipsMeta.count, + updatedAt: person.updatedAt, + })); + + return ( + <> + + + + ); +} + +export default function AsyncDataListGraphQLPage() { + return ( + + + + ); +} diff --git a/example/pages/AsyncDataListRestPage.tsx b/example/pages/AsyncDataListRestPage.tsx new file mode 100644 index 0000000..24e710d --- /dev/null +++ b/example/pages/AsyncDataListRestPage.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { useFetch } from 'react-async'; +import { useHistory, useLocation } from 'react-router-dom'; +import { useA11yRouteChange, useDocumentTitle } from 'use-patternfly'; +import { AsyncDataListRestHeader, IStarWarsPerson, StarWarsPeople } from '../components'; + +export interface IStarWarsRestPerson { + name: string; + height: string; + mass: string; + hair_color: string; + skin_color: string; + eye_color: string; + birth_year: string; + gender: string; + homeworld: string; + films: string[]; + species: string[]; + vehicles: string[]; + starships: string[]; + created: Date; + edited: Date; + url: string; +} + +interface IStarWarsPeopleResponse { + count: number; + next: string; + previous?: string; + results: IStarWarsRestPerson[]; +} + +export default function AsyncDataListRestPage() { + useDocumentTitle('Async Data List (REST)'); + useA11yRouteChange(); + const location = useLocation(); + const history = useHistory(); + const searchParams = new URLSearchParams(location.search); + const page = parseInt(searchParams.get('page') || '', 10) || 1; + + const handlePageChange = React.useCallback( + (newPage: number) => { + searchParams.set('page', newPage.toString()); + history.push({ + search: searchParams.toString(), + }); + }, + [searchParams, history] + ); + + const { data, isPending } = useFetch( + `https://swapi.co/api/people/?page=${page}`, + { headers: { Accept: 'application/json' } } + ); + + const { results = [], count: total = 0 } = data || {}; + + const people = results.map(person => ({ + name: person.name, + mass: person.mass, + gender: person.gender, + films: person.films.length, + vehicles: person.vehicles.length, + starships: person.starships.length, + updatedAt: person.edited, + })); + + return ( + <> + + alert('Sorry, SWApi doesn\'t support this')} + loading={isPending} + /> + + ); +} diff --git a/example/pages/InstallationPage.tsx b/example/pages/InstallationPage.tsx index 315f3e8..2edf45e 100644 --- a/example/pages/InstallationPage.tsx +++ b/example/pages/InstallationPage.tsx @@ -1,11 +1,9 @@ import * as React from 'react'; -import { useA11yRouteContainer, useDocumentTitle } from 'use-patternfly'; +import { useA11yRouteChange, useDocumentTitle } from 'use-patternfly'; import { Installation } from '../components'; export default function InstallationPage() { - const a11yContainerProps = useA11yRouteContainer(); + useA11yRouteChange(); useDocumentTitle('Installation'); - return ( - - ); + return ; } diff --git a/example/pages/NotFound.tsx b/example/pages/NotFound.tsx index 1f2a0b1..3ad5617 100644 --- a/example/pages/NotFound.tsx +++ b/example/pages/NotFound.tsx @@ -1,17 +1,20 @@ import * as React from 'react'; import { NavLink } from 'react-router-dom'; import { Alert, PageSection } from '@patternfly/react-core'; -import { useA11yRouteContainer, useDocumentTitle } from 'use-patternfly'; +import { useA11yRouteChange, useDocumentTitle } from 'use-patternfly'; const NotFound: React.FunctionComponent = () => { - const a11yContainerProps = useA11yRouteContainer(); + useA11yRouteChange(); useDocumentTitle('Page not found'); return ( - -
- Take me home + + +
+ + Take me home +
); -} +}; export default NotFound; diff --git a/example/pages/OverviewPage.tsx b/example/pages/OverviewPage.tsx index 6dc0827..255b2dd 100644 --- a/example/pages/OverviewPage.tsx +++ b/example/pages/OverviewPage.tsx @@ -1,11 +1,9 @@ import * as React from 'react'; -import { useA11yRouteContainer, useDocumentTitle } from 'use-patternfly'; +import { useA11yRouteChange, useDocumentTitle } from 'use-patternfly'; import { Overview } from '../components'; export default function OverviewPage() { - const a11yContainerProps = useA11yRouteContainer(); + useA11yRouteChange(); useDocumentTitle('Overview'); - return ( - - ); + return ; } diff --git a/example/pages/UsagePage.tsx b/example/pages/UsagePage.tsx index 9864620..192841d 100644 --- a/example/pages/UsagePage.tsx +++ b/example/pages/UsagePage.tsx @@ -1,11 +1,9 @@ import * as React from 'react'; -import { useA11yRouteContainer, useDocumentTitle } from 'use-patternfly'; +import { useA11yRouteChange, useDocumentTitle } from 'use-patternfly'; import { Usage } from '../components'; export default function UsagePage() { - const a11yContainerProps = useA11yRouteContainer(); + useA11yRouteChange(); useDocumentTitle('Usage'); - return ( - - ); + return ; } diff --git a/example/yarn.lock b/example/yarn.lock index f1ceeeb..31a4d3e 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -2,6 +2,24 @@ # yarn lockfile v1 +"@apollo/react-common@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@apollo/react-common/-/react-common-3.1.2.tgz#19feb1d264c1e82e9610f68298a9dd89513060ea" + integrity sha512-6+gTeBZoIyCE6VnHD2EI9Wz+Dm05MBxplUTmVgswzvTe05lUO78SxGgB3XJc9GM/TmNexgCc0eS84vUpG/f6Tg== + dependencies: + ts-invariant "^0.4.4" + tslib "^1.10.0" + +"@apollo/react-hooks@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@apollo/react-hooks/-/react-hooks-3.1.2.tgz#3f889f9448ebb32faf164117f1f63d9ffa18f5d9" + integrity sha512-PV5u40E9iwfwM7u61r2P9PTjcGaM3zRwiwrJGDKOKaOn1Y9wTHhKOVEQa7YOsCWciSaMVK1slpKMvQbD2Ypqtw== + dependencies: + "@apollo/react-common" "^3.1.2" + "@wry/equality" "^0.1.9" + ts-invariant "^0.4.4" + tslib "^1.10.0" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" @@ -742,6 +760,11 @@ "@parcel/utils" "^1.11.0" physical-cpu-count "^2.0.0" +"@types/node@>=6": + version "12.7.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.12.tgz#7c6c571cc2f3f3ac4a59a5f2bd48f5bdbc8653cc" + integrity sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ== + "@types/prop-types@*": version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" @@ -767,6 +790,26 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/zen-observable@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" + integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== + +"@wry/context@^0.4.0": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.4.4.tgz#e50f5fa1d6cfaabf2977d1fda5ae91717f8815f8" + integrity sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag== + dependencies: + "@types/node" ">=6" + tslib "^1.9.3" + +"@wry/equality@^0.1.2", "@wry/equality@^0.1.9": + version "0.1.9" + resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.9.tgz#b13e18b7a8053c6858aa6c85b54911fb31e3a909" + integrity sha512-mB6ceGjpMGz1ZTza8HYnrPGos2mC6So4NhS1PtZ8s4Qt0K7fBiIGhpSxUbQmhwcSWE3no+bYxmI2OL6KuXYmoQ== + dependencies: + tslib "^1.9.3" + abab@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.2.tgz#a2fba1b122c69a85caa02d10f9270c7219709a9d" @@ -852,6 +895,101 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +apollo-boost@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/apollo-boost/-/apollo-boost-0.4.4.tgz#7c278dac6cb6fa3f2f710c56baddc6e3ae730651" + integrity sha512-ASngBvazmp9xNxXfJ2InAzfDwz65o4lswlEPrWoN35scXmCz8Nz4k3CboUXbrcN/G0IExkRf/W7o9Rg0cjEBqg== + dependencies: + apollo-cache "^1.3.2" + apollo-cache-inmemory "^1.6.3" + apollo-client "^2.6.4" + apollo-link "^1.0.6" + apollo-link-error "^1.0.3" + apollo-link-http "^1.3.1" + graphql-tag "^2.4.2" + ts-invariant "^0.4.0" + tslib "^1.9.3" + +apollo-cache-inmemory@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz#826861d20baca4abc45f7ca7a874105905b8525d" + integrity sha512-S4B/zQNSuYc0M/1Wq8dJDTIO9yRgU0ZwDGnmlqxGGmFombOZb9mLjylewSfQKmjNpciZ7iUIBbJ0mHlPJTzdXg== + dependencies: + apollo-cache "^1.3.2" + apollo-utilities "^1.3.2" + optimism "^0.10.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + +apollo-cache@1.3.2, apollo-cache@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.2.tgz#df4dce56240d6c95c613510d7e409f7214e6d26a" + integrity sha512-+KA685AV5ETEJfjZuviRTEImGA11uNBp/MJGnaCvkgr+BYRrGLruVKBv6WvyFod27WEB2sp7SsG8cNBKANhGLg== + dependencies: + apollo-utilities "^1.3.2" + tslib "^1.9.3" + +apollo-client@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.4.tgz#872c32927263a0d34655c5ef8a8949fbb20b6140" + integrity sha512-oWOwEOxQ9neHHVZrQhHDbI6bIibp9SHgxaLRVPoGvOFy7OH5XUykZE7hBQAVxq99tQjBzgytaZffQkeWo1B4VQ== + dependencies: + "@types/zen-observable" "^0.8.0" + apollo-cache "1.3.2" + apollo-link "^1.0.0" + apollo-utilities "1.3.2" + symbol-observable "^1.0.2" + ts-invariant "^0.4.0" + tslib "^1.9.3" + zen-observable "^0.8.0" + +apollo-link-error@^1.0.3: + version "1.1.12" + resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.12.tgz#e24487bb3c30af0654047611cda87038afbacbf9" + integrity sha512-psNmHyuy3valGikt/XHJfe0pKJnRX19tLLs6P6EHRxg+6q6JMXNVLYPaQBkL0FkwdTCB0cbFJAGRYCBviG8TDA== + dependencies: + apollo-link "^1.2.13" + apollo-link-http-common "^0.2.15" + tslib "^1.9.3" + +apollo-link-http-common@^0.2.15: + version "0.2.15" + resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.15.tgz#304e67705122bf69a9abaded4351b10bc5efd6d9" + integrity sha512-+Heey4S2IPsPyTf8Ag3PugUupASJMW894iVps6hXbvwtg1aHSNMXUYO5VG7iRHkPzqpuzT4HMBanCTXPjtGzxg== + dependencies: + apollo-link "^1.2.13" + ts-invariant "^0.4.0" + tslib "^1.9.3" + +apollo-link-http@^1.3.1: + version "1.5.16" + resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.16.tgz#44fe760bcc2803b8a7f57fc9269173afb00f3814" + integrity sha512-IA3xA/OcrOzINRZEECI6IdhRp/Twom5X5L9jMehfzEo2AXdeRwAMlH5LuvTZHgKD8V1MBnXdM6YXawXkTDSmJw== + dependencies: + apollo-link "^1.2.13" + apollo-link-http-common "^0.2.15" + tslib "^1.9.3" + +apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.13: + version "1.2.13" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.13.tgz#dff00fbf19dfcd90fddbc14b6a3f9a771acac6c4" + integrity sha512-+iBMcYeevMm1JpYgwDEIDt/y0BB7VWyvlm/7x+TIPNLHCTCMgcEgDuW5kH86iQZWo0I7mNwQiTOz+/3ShPFmBw== + dependencies: + apollo-utilities "^1.3.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + zen-observable-ts "^0.8.20" + +apollo-utilities@1.3.2, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9" + integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg== + dependencies: + "@wry/equality" "^0.1.2" + fast-json-stable-stringify "^2.0.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -2345,6 +2483,18 @@ grapheme-breaker@^0.3.2: brfs "^1.2.0" unicode-trie "^0.3.1" +graphql-tag@^2.4.2: + version "2.10.1" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02" + integrity sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg== + +graphql@^14.5.8: + version "14.5.8" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.8.tgz#504f3d3114cb9a0a3f359bbbcf38d9e5bf6a6b3c" + integrity sha512-MMwmi0zlVLQKLdGiMfWkgQD7dY/TUKt4L+zgJ/aR0Howebod3aNgP5JkgvAULiR2HPVZaP2VEElqtdidHweLkg== + dependencies: + iterall "^1.2.2" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -2855,6 +3005,11 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= +iterall@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" + integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== + js-levenshtein@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" @@ -3478,6 +3633,13 @@ opn@^5.1.0: dependencies: is-wsl "^1.1.0" +optimism@^0.10.0: + version "0.10.3" + resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.10.3.tgz#163268fdc741dea2fb50f300bedda80356445fd7" + integrity sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw== + dependencies: + "@wry/context" "^0.4.0" + optionator@^0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" @@ -4837,6 +4999,11 @@ svgo@^1.0.0, svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" +symbol-observable@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + symbol-tree@^3.2.2: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -4966,6 +5133,18 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" +ts-invariant@^0.4.0, ts-invariant@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" + integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA== + dependencies: + tslib "^1.9.3" + +tslib@^1.10.0, tslib@^1.9.3: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -5281,3 +5460,16 @@ yallist@^3.0.0, yallist@^3.0.3: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +zen-observable-ts@^0.8.20: + version "0.8.20" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz#44091e335d3fcbc97f6497e63e7f57d5b516b163" + integrity sha512-2rkjiPALhOtRaDX6pWyNqK1fnP5KkJJybYebopNSn6wDG1lxBoFs2+nwwXKoA6glHIrtwrfBBy6da0stkKtTAA== + dependencies: + tslib "^1.9.3" + zen-observable "^0.8.0" + +zen-observable@^0.8.0: + version "0.8.14" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.14.tgz#d33058359d335bc0db1f0af66158b32872af3bf7" + integrity sha512-kQz39uonEjEESwh+qCi83kcC3rZJGh4mrZW7xjkSQYXkq//JZHTtKo+6yuVloTgMtzsIWOJrjIrKvk/dqm0L5g== diff --git a/package.json b/package.json index 1179a3c..059c1b2 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@patternfly/react-core": "^3.77.2", "@patternfly/react-icons": "^3.10.15", "@patternfly/react-styles": "^3.5.7", + "date-fns": "^2.4.1", "react": ">=16", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", @@ -62,6 +63,7 @@ "babel-loader": "^8.0.6", "codacy-coverage": "^3.4.0", "coveralls": "^3.0.6", + "date-fns": "^2.4.1", "eslint-config-react-app": "5.0.1", "husky": "^3.0.8", "react": "^16.10.2", diff --git a/src/AppLayout.tsx b/src/AppLayout.tsx index 38713d2..7f4e62f 100644 --- a/src/AppLayout.tsx +++ b/src/AppLayout.tsx @@ -32,6 +32,7 @@ export interface IAppLayoutProps navGroupsStyle?: 'grouped' | 'expandable'; startWithOpenNav?: boolean; theme?: 'dark' | 'light'; + mainContainerId?: string; } export const AppLayout: React.FunctionComponent = ({ @@ -44,6 +45,7 @@ export const AppLayout: React.FunctionComponent = ({ avatar, startWithOpenNav = true, theme = 'dark', + mainContainerId = 'main-container', children, }) => { const [isNavOpen, setIsNavOpen] = React.useState(startWithOpenNav); @@ -155,7 +157,7 @@ export const AppLayout: React.FunctionComponent = ({ return ( [2]; +} +export const FormatDate: React.FunctionComponent = ({ + date, + format: formatTpl = 'Pp', + options, +}) => { + date = typeof date === 'string' ? new Date(date) : date; + return <>{format(date, formatTpl, options)}; +}; diff --git a/src/FormatDistance.tsx b/src/FormatDistance.tsx new file mode 100644 index 0000000..4f64370 --- /dev/null +++ b/src/FormatDistance.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { formatDistance } from 'date-fns'; +import { ArgumentsType } from '../types'; + +export interface IFormatDistanceProps { + date: string | Date | number; + base?: Date; + options?: ArgumentsType[2]; +} +export const FormatDistance: React.FunctionComponent = ({ + date, + base = new Date(), + options, +}) => { + date = typeof date === 'string' ? new Date(date) : date; + return <>{formatDistance(date, base, options)}; +}; diff --git a/src/FormatRelative.tsx b/src/FormatRelative.tsx new file mode 100644 index 0000000..6ffde83 --- /dev/null +++ b/src/FormatRelative.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { formatRelative } from 'date-fns'; +import { ArgumentsType } from '../types'; + +export interface IFormatRelativeProps { + date: string | Date | number; + base?: Date; + options?: ArgumentsType[2]; +} + +export const FormatRelative: React.FunctionComponent = ({ + date, + base = new Date(), + options, +}) => { + date = typeof date === 'string' ? new Date(date) : date; + return <>{formatRelative(date, base, options)}; +}; diff --git a/src/NotFound.tsx b/src/NotFound.tsx index 95d71b4..5717c25 100644 --- a/src/NotFound.tsx +++ b/src/NotFound.tsx @@ -1,14 +1,14 @@ import * as React from 'react'; import { NavLink } from 'react-router-dom'; import { Alert, PageSection } from '@patternfly/react-core'; -import { useA11yRouteContainer } from './useA11yRoute'; +import { useA11yRouteChange } from './useA11yRoute'; import { useDocumentTitle } from './useDocumentTitle'; export const NotFound: React.FunctionComponent = () => { - const a11yContainerProps = useA11yRouteContainer(); + useA11yRouteChange(); useDocumentTitle('Page not found'); return ( - +
diff --git a/src/index.ts b/src/index.ts index 459ff80..c673454 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,9 @@ export * from './AppLayout'; export * from './AppNavExpandable'; export * from './AppNavGroup'; export * from './AppNavItem'; +export * from './FormatDate'; +export * from './FormatDistance'; +export * from './FormatRelative'; export * from './LazyRoute'; export * from './Loading'; export * from './NotFound'; diff --git a/src/useA11yRoute.ts b/src/useA11yRoute.ts index c7489e4..7995cc2 100644 --- a/src/useA11yRoute.ts +++ b/src/useA11yRoute.ts @@ -1,5 +1,4 @@ import * as React from 'react'; -import { useRouteMatch } from 'react-router'; import { LastLocationType, useLastLocation } from 'react-router-last-location'; export function accessibleRouteChangeHandler(id: string, timeout = 50) { @@ -11,18 +10,12 @@ export function accessibleRouteChangeHandler(id: string, timeout = 50) { }, timeout); } -export const useA11yRouteContainerId = () => { - const { path } = useRouteMatch()!; - return `route-content-${path}`; -}; - /** * a custom hook for sending focus to the primary content container * after a view has loaded so that subsequent press of tab key * sends focus directly to relevant content */ -export const useA11yRouteChange = () => { - const id = useA11yRouteContainerId(); +export const useA11yRouteChange = (id = 'main-container') => { const lastNavigation = useLastLocation(); const previousNavigation = React.useRef(); React.useEffect(() => { @@ -40,10 +33,3 @@ export const useA11yRouteChange = () => { }; }, [id, lastNavigation, previousNavigation]); }; - -export const useA11yRouteContainer = () => { - useA11yRouteChange(); - const id = useA11yRouteContainerId(); - const tabIndex = -1; - return { id, tabIndex }; -}; diff --git a/test/useA11yRoute.test.tsx b/test/useA11yRoute.test.tsx index 2e6915a..0531108 100644 --- a/test/useA11yRoute.test.tsx +++ b/test/useA11yRoute.test.tsx @@ -2,16 +2,16 @@ import * as React from 'react'; import { Link, Route, Switch } from 'react-router-dom'; import { waitForElement } from '@testing-library/dom'; import { render, fireEvent } from '@test/setup'; -import { useA11yRouteContainer } from '@src'; +import { useA11yRouteChange } from '@src'; const SamplePage = () => { - const a11yContainerProps = useA11yRouteContainer(); + useA11yRouteChange('test-focus'); const [focused, setFocused] = React.useState(false); const handleFocus = () => { setFocused(true); }; return ( -
+
{focused ? (
I'm focused!
) : ( diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..6fdc3e7 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1 @@ +export type ArgumentsType any> = T extends (...args: infer A) => any ? A : never; diff --git a/yarn.lock b/yarn.lock index 89063aa..8faf930 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4653,6 +4653,11 @@ data-urls@^1.0.0, data-urls@^1.1.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" +date-fns@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.4.1.tgz#b53f9bb65ae6bd9239437035710e01cf383b625e" + integrity sha512-2RhmH/sjDSCYW2F3ZQxOUx/I7PvzXpi89aQL2d3OAxSTwLx6NilATeUbe0menFE3Lu5lFkOFci36ivimwYHHxw== + date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"