Skip to content

Commit

Permalink
feat(v2): add and use NavigationTabs template components for admin fo…
Browse files Browse the repository at this point in the history
…rm tab navigation (#4601)

* feat: add FauxTabs for navigating admin form builder instead of Tabs

Usage of Tab component was messing with the navigation prompt, since the Tab component's tab index onChange callback will fire continuously even if user dismisses the prompt.

This was a really hard bug to drill down to...

* feat: move and rename FauxTabs to NavigationTabs template component

* style: add disabled handling to Tabs component

* feat: generify NavigationTabList for use in FormResultsNavbar too

* feat(FormResultsNavbar): use NavigationTabs over chakra tabs

* fix: add inline flex to Tab style

so the NavigationTab component has full height

* ref: remove need for regrouping routes in constant

* feat: add disabled state according to design

* feat(AdminFormNavbar): remove disabled state when forms are loading

no need, users should be able to navigate regardless

* fix: update AdminNavbar stories to fit new implementation

* fix: unhide results subroute in navbar even if viewonly

* fix: update admin form stories affected by navigation tabs change

correctly prefixing the routes in affected stories
  • Loading branch information
karrui authored Aug 25, 2022
1 parent b38db39 commit 085446b
Show file tree
Hide file tree
Showing 16 changed files with 251 additions and 175 deletions.
19 changes: 19 additions & 0 deletions frontend/src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,25 @@ export const ADMINFORM_SETTINGS_SUBROUTE = 'settings'
export const ADMINFORM_RESULTS_SUBROUTE = 'results'
export const ADMINFORM_PREVIEW_ROUTE = 'preview'

/**
* Regex for active path matching on adminform builder routes/subroutes.
* @example Breakdown of regex:
* `${ADMINFORM_ROUTE}/` - non-capturing start of route
* `([a-fA-F0-9]{24})` - formId capture group, will be match[1]
* `(/${ADMINFORM_SETTINGS_SUBROUTE}|/${ADMINFORM_RESULTS_SUBROUTE})` - subroute capture group, will be match[2]
* `?` - optional subroute capture group
* `/?` - optional trailing slash, also allows for ADMINFORM_BUILD_SUBROUTE to match
*/
export const ACTIVE_ADMINFORM_BUILDER_ROUTE_REGEX = new RegExp(
`${ADMINFORM_ROUTE}/([a-fA-F0-9]{24})(/${ADMINFORM_SETTINGS_SUBROUTE}|/${ADMINFORM_RESULTS_SUBROUTE})?/?`,
'i',
)

/** Responses tab has no subroute, its the index results route. */
export const RESULTS_RESPONSES_SUBROUTE = ''
export const RESULTS_FEEDBACK_SUBROUTE = 'feedback'

export const ACTIVE_ADMINFORM_RESULTS_ROUTE_REGEX = new RegExp(
`${ADMINFORM_ROUTE}/([a-fA-F0-9]{24})/${ADMINFORM_RESULTS_SUBROUTE}(/${RESULTS_FEEDBACK_SUBROUTE})?/?`,
'i',
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getUser } from '~/mocks/msw/handlers/user'

import {
ADMINFORM_RESULTS_SUBROUTE,
ADMINFORM_ROUTE,
RESULTS_FEEDBACK_SUBROUTE,
} from '~constants/routes'
import { getMobileViewParameters, viewports } from '~utils/storybook'
Expand Down Expand Up @@ -42,10 +43,15 @@ export default {
const Template: Story = () => {
return (
<MemoryRouter
initialEntries={['/61540ece3d4a6e50ac0cc6ff/results/feedback']}
initialEntries={[
`${ADMINFORM_ROUTE}/61540ece3d4a6e50ac0cc6ff/${ADMINFORM_RESULTS_SUBROUTE}/${RESULTS_FEEDBACK_SUBROUTE}`,
]}
>
<Routes>
<Route path="/:formId" element={<AdminFormLayout />}>
<Route
path={`${ADMINFORM_ROUTE}/:formId`}
element={<AdminFormLayout />}
>
<Route
path={ADMINFORM_RESULTS_SUBROUTE}
element={<FormResultsLayout />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getUser } from '~/mocks/msw/handlers/user'

import {
ADMINFORM_RESULTS_SUBROUTE,
ADMINFORM_ROUTE,
RESULTS_FEEDBACK_SUBROUTE,
} from '~constants/routes'
import { getMobileViewParameters, viewports } from '~utils/storybook'
Expand Down Expand Up @@ -51,9 +52,16 @@ const MOCK_KEYPAIR = {

const Template: Story = () => {
return (
<MemoryRouter initialEntries={['/12345/results']}>
<MemoryRouter
initialEntries={[
`${ADMINFORM_ROUTE}/61540ece3d4a6e50ac0cc6ff/${ADMINFORM_RESULTS_SUBROUTE}`,
]}
>
<Routes>
<Route path="/:formId" element={<AdminFormLayout />}>
<Route
path={`${ADMINFORM_ROUTE}/:formId`}
element={<AdminFormLayout />}
>
<Route
path={ADMINFORM_RESULTS_SUBROUTE}
element={<FormResultsLayout />}
Expand Down
14 changes: 11 additions & 3 deletions frontend/src/features/admin-form/AdminFormSettingsPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { getFreeSmsQuota } from '~/mocks/msw/handlers/admin-form/twilio'
import { getUser } from '~/mocks/msw/handlers/user'

import { ADMINFORM_ROUTE, ADMINFORM_SETTINGS_SUBROUTE } from '~constants/routes'
import formsgSdk from '~utils/formSdk'
import { viewports } from '~utils/storybook'

Expand All @@ -32,10 +33,17 @@ export default {
// MemoryRouter is used so react-router-dom#Link components can work
// (and also to force the initial tab the page renders to be the settings tab).
return (
<MemoryRouter initialEntries={['/12345/settings']}>
<MemoryRouter
initialEntries={[
`${ADMINFORM_ROUTE}/61540ece3d4a6e50ac0cc6ff/${ADMINFORM_SETTINGS_SUBROUTE}`,
]}
>
<Routes>
<Route path={'/:formId'} element={<AdminFormLayout />}>
<Route path="settings" element={storyFn()} />
<Route
path={`${ADMINFORM_ROUTE}/:formId`}
element={<AdminFormLayout />}
>
<Route path={ADMINFORM_SETTINGS_SUBROUTE} element={storyFn()} />
</Route>
</Routes>
</MemoryRouter>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { TabPanel, TabPanels, Tabs } from '@chakra-ui/react'
import { Meta, Story } from '@storybook/react'

import { DateString } from '~shared/types/generic'

import { getMobileViewParameters } from '~utils/storybook'
import { getMobileViewParameters, StoryRouter } from '~utils/storybook'

import { AdminFormNavbar, AdminFormNavbarProps } from './AdminFormNavbar'

Expand All @@ -15,20 +14,7 @@ const MOCK_FORM: AdminFormNavbarProps['formInfo'] = {
export default {
title: 'Features/AdminForm/AdminFormNavbar',
component: AdminFormNavbar,
decorators: [
(storyFn) => {
return (
<Tabs>
{storyFn()}
<TabPanels>
<TabPanel></TabPanel>
<TabPanel></TabPanel>
<TabPanel></TabPanel>
</TabPanels>
</Tabs>
)
},
],
decorators: [StoryRouter({ path: 'test', initialEntries: ['/test'] })],
parameters: {
layout: 'fullscreen',
},
Expand All @@ -53,11 +39,7 @@ Skeleton.args = {
formInfo: undefined,
}

export const Mobile: Story<AdminFormNavbarProps> = (args) => (
<Tabs variant="line-dark">
<AdminFormNavbar {...args} />
</Tabs>
)
export const Mobile = Template.bind({})
Mobile.args = {
formInfo: {
...MOCK_FORM,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from 'react'
import { useCallback, useMemo } from 'react'
import {
BiDotsHorizontalRounded,
BiHelpCircle,
Expand All @@ -7,6 +7,7 @@ import {
BiShow,
BiUserPlus,
} from 'react-icons/bi'
import { useLocation } from 'react-router-dom'
import {
Box,
ButtonGroup,
Expand All @@ -17,18 +18,24 @@ import {
Flex,
Grid,
GridItem,
TabList,
useBreakpointValue,
useDisclosure,
} from '@chakra-ui/react'

import { AdminFormDto } from '~shared/types/form/form'

import { FORM_GUIDE } from '~constants/links'
import {
ACTIVE_ADMINFORM_BUILDER_ROUTE_REGEX,
ADMINFORM_BUILD_SUBROUTE,
ADMINFORM_RESULTS_SUBROUTE,
ADMINFORM_SETTINGS_SUBROUTE,
} from '~constants/routes'
import { useDraggable } from '~hooks/useDraggable'
import Button, { ButtonProps } from '~components/Button'
import IconButton from '~components/IconButton'
import { Tab } from '~components/Tabs'
import Tooltip from '~components/Tooltip'
import { NavigationTab, NavigationTabList } from '~templates/NavigationTabs'

import { AdminFormNavbarDetails } from './AdminFormNavbarDetails'

Expand Down Expand Up @@ -60,6 +67,21 @@ export const AdminFormNavbar = ({
}: AdminFormNavbarProps): JSX.Element => {
const { ref, onMouseDown } = useDraggable<HTMLDivElement>()
const { isOpen, onClose, onOpen } = useDisclosure()
const { pathname } = useLocation()

const tabResponsiveVariant = useBreakpointValue({
base: 'line-dark',
xs: 'line-dark',
lg: 'line-light',
})

const checkTabActive = useCallback(
(to: string) => {
const match = pathname.match(ACTIVE_ADMINFORM_BUILDER_ROUTE_REGEX)
return (match?.[2] ?? '/') === `/${to}`
},
[pathname],
)

const mobileDrawerExtraButtonProps: Partial<ButtonProps> = useMemo(
() => ({
Expand Down Expand Up @@ -92,7 +114,7 @@ export const AdminFormNavbar = ({
mb="1px"
bg="white"
zIndex="docked"
flex={1}
flex={0}
>
<GridItem
display="flex"
Expand All @@ -116,25 +138,39 @@ export const AdminFormNavbar = ({
</Box>
<AdminFormNavbarDetails formInfo={formInfo} />
</GridItem>
<TabList
<NavigationTabList
variant={tabResponsiveVariant}
ref={ref}
onMouseDown={onMouseDown}
pt={{ base: '0.625rem', lg: 0 }}
px={{ base: '1.25rem', lg: '1rem' }}
w={{ base: '100vw', lg: 'initial' }}
gridArea="tabs"
borderBottom="none"
justifyContent={{ base: 'flex-start', lg: 'center' }}
justifySelf={{ base: 'flex-start', lg: 'center' }}
alignSelf="center"
>
<Tab hidden={viewOnly} isDisabled={!formInfo}>
<NavigationTab
hidden={viewOnly}
to={ADMINFORM_BUILD_SUBROUTE}
isActive={checkTabActive(ADMINFORM_BUILD_SUBROUTE)}
>
Create
</Tab>
<Tab hidden={viewOnly} isDisabled={!formInfo}>
</NavigationTab>
<NavigationTab
hidden={viewOnly}
to={ADMINFORM_SETTINGS_SUBROUTE}
isActive={checkTabActive(ADMINFORM_SETTINGS_SUBROUTE)}
>
Settings
</Tab>
<Tab isDisabled={!formInfo}>Results</Tab>
</TabList>
</NavigationTab>
<NavigationTab
to={ADMINFORM_RESULTS_SUBROUTE}
isActive={checkTabActive(ADMINFORM_RESULTS_SUBROUTE)}
>
Results
</NavigationTab>
</NavigationTabList>
<Flex
py="0.625rem"
pl="1rem"
Expand Down
Loading

0 comments on commit 085446b

Please sign in to comment.