Skip to content

Commit

Permalink
feat(app,components): implement ODD language setting toggle
Browse files Browse the repository at this point in the history
adds the ODD language setting toggle to the robot settings page

closes PLAT-506
  • Loading branch information
brenthagen committed Oct 24, 2024
1 parent 53f86a5 commit d5e9932
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 29 deletions.
92 changes: 92 additions & 0 deletions app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'

import {
BORDERS,
COLORS,
CURSOR_POINTER,
DIRECTION_COLUMN,
Flex,
SPACING,
StyledText,
} from '@opentrons/components'

import { LANGUAGES } from '/app/i18n'
import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation'
import { getAppLanguage, updateConfigValue } from '/app/redux/config'

import type { Dispatch } from '/app/redux/types'
import type { SetSettingOption } from './types'

interface LabelProps {
isSelected?: boolean
}

const SettingButton = styled.input`
display: none;
`

const SettingButtonLabel = styled.label<LabelProps>`
padding: ${SPACING.spacing24};
border-radius: ${BORDERS.borderRadius16};
cursor: ${CURSOR_POINTER};
background: ${({ isSelected }) =>
isSelected === true ? COLORS.blue50 : COLORS.blue35};
color: ${({ isSelected }) => isSelected === true && COLORS.white};
`

interface LanguageSettingProps {
setCurrentOption: SetSettingOption
}

export function LanguageSetting({
setCurrentOption,
}: LanguageSettingProps): JSX.Element {
const { t } = useTranslation('app_settings')
const dispatch = useDispatch<Dispatch>()

const appLanguage = useSelector(getAppLanguage)

const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
dispatch(updateConfigValue('language.appLanguage', event.target.value))
}

return (
<Flex flexDirection={DIRECTION_COLUMN}>
<ChildNavigation
header={t('language')}
onClickBack={() => {
setCurrentOption(null)
}}
/>
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing8}
marginTop="7.75rem"
padding={`${SPACING.spacing16} ${SPACING.spacing40} ${SPACING.spacing40} ${SPACING.spacing40}`}
>
{LANGUAGES.map(lng => (
<React.Fragment key={`language_setting_${lng.name}`}>
<SettingButton
id={lng.name}
type="radio"
value={lng.value}
checked={lng.value === appLanguage}
onChange={handleChange}
/>
<SettingButtonLabel
htmlFor={lng.name}
isSelected={lng.value === appLanguage}
>
<StyledText oddStyle="level4HeaderSemiBold">
{lng.name}
</StyledText>
</SettingButtonLabel>
</React.Fragment>
))}
</Flex>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type * as React from 'react'
import { fireEvent, screen } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import '@testing-library/jest-dom/vitest'

import {
i18n,
US_ENGLISH_DISPLAY_NAME,
US_ENGLISH,
SIMPLIFIED_CHINESE_DISPLAY_NAME,
SIMPLIFIED_CHINESE,
} from '/app/i18n'
import { getAppLanguage, updateConfigValue } from '/app/redux/config'
import { renderWithProviders } from '/app/__testing-utils__'

import { LanguageSetting } from '../LanguageSetting'

vi.mock('/app/redux/config')

const mockSetCurrentOption = vi.fn()

const render = (props: React.ComponentProps<typeof LanguageSetting>) => {
return renderWithProviders(<LanguageSetting {...props} />, {
i18nInstance: i18n,
})
}

describe('LanguageSetting', () => {
let props: React.ComponentProps<typeof LanguageSetting>
beforeEach(() => {
props = {
setCurrentOption: mockSetCurrentOption,
}
vi.mocked(getAppLanguage).mockReturnValue(US_ENGLISH)
})

it('should render text and buttons', () => {
render(props)
screen.getByText('Language')
screen.getByText(US_ENGLISH_DISPLAY_NAME)
screen.getByText(SIMPLIFIED_CHINESE_DISPLAY_NAME)
})

it('should call mock function when tapping a language button', () => {
render(props)
const button = screen.getByText(SIMPLIFIED_CHINESE_DISPLAY_NAME)
fireEvent.click(button)
expect(updateConfigValue).toHaveBeenCalledWith(
'language.appLanguage',
SIMPLIFIED_CHINESE
)
})

it('should call mock function when tapping back button', () => {
render(props)
const button = screen.getByRole('button')
fireEvent.click(button)
expect(props.setCurrentOption).toHaveBeenCalled()
})
})
1 change: 1 addition & 0 deletions app/src/organisms/ODD/RobotSettingsDashboard/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './DeviceReset'
export * from './LanguageSetting'
export * from './NetworkSettings/RobotSettingsJoinOtherNetwork'
export * from './NetworkSettings/RobotSettingsSelectAuthenticationType'
export * from './NetworkSettings/RobotSettingsSetWifiCred'
Expand Down
1 change: 1 addition & 0 deletions app/src/organisms/ODD/RobotSettingsDashboard/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export type SettingOption =
| 'RobotSettingsSetWifiCred'
| 'RobotSettingsWifi'
| 'RobotSettingsWifiConnect'
| 'LanguageSetting'

export type SetSettingOption = (option: SettingOption | null) => void
6 changes: 3 additions & 3 deletions app/src/pages/Desktop/AppSettings/GeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function GeneralSettings(): JSX.Element {

const enableLocalization = useFeatureFlag('enableLocalization')
const appLanguage = useSelector(getAppLanguage)
const currentOption = LANGUAGES.find(lng => lng.value === appLanguage)
const currentLanguageOption = LANGUAGES.find(lng => lng.value === appLanguage)

const handleDropdownClick = (value: string): void => {
dispatch(updateConfigValue('language.appLanguage', value))
Expand Down Expand Up @@ -277,7 +277,7 @@ export function GeneralSettings(): JSX.Element {
</TertiaryButton>
</Flex>
<Divider marginY={SPACING.spacing24} />
{enableLocalization && currentOption != null ? (
{enableLocalization && currentLanguageOption != null ? (
<>
<Flex
flexDirection={DIRECTION_ROW}
Expand All @@ -296,7 +296,7 @@ export function GeneralSettings(): JSX.Element {
</Flex>
<DropdownMenu
filterOptions={LANGUAGES}
currentOption={currentOption}
currentOption={currentLanguageOption}
onClick={handleDropdownClick}
title={t('language')}
width="9.5rem"
Expand Down
43 changes: 19 additions & 24 deletions app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useContext } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { I18nContext, useTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'

import {
Expand All @@ -21,18 +20,19 @@ import {
TYPOGRAPHY,
} from '@opentrons/components'

import { LANGUAGES } from '/app/i18n'
import { getLocalRobot, getRobotApiVersion } from '/app/redux/discovery'
import { getRobotUpdateAvailable } from '/app/redux/robot-update'
import { useErrorRecoverySettingsToggle } from '/app/resources/errorRecovery'
import {
DEV_INTERNAL_FLAGS,
getAppLanguage,
getApplyHistoricOffsets,
getDevtoolsEnabled,
getFeatureFlags,
toggleDevInternalFlag,
toggleDevtools,
toggleHistoricOffsets,
updateConfigValue,
useFeatureFlag,
} from '/app/redux/config'
import { InlineNotification } from '/app/atoms/InlineNotification'
Expand Down Expand Up @@ -88,6 +88,10 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element {
const { lightsEnabled, toggleLights } = useLEDLights(robotName)
const { toggleERSettings, isEREnabled } = useErrorRecoverySettingsToggle()

const appLanguage = useSelector(getAppLanguage)
const currentLanguageOption = LANGUAGES.find(lng => lng.value === appLanguage)
const enableLocalization = useFeatureFlag('enableLocalization')

return (
<Flex flexDirection={DIRECTION_COLUMN}>
<Navigation />
Expand Down Expand Up @@ -139,6 +143,18 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element {
</Flex>
}
/>
{enableLocalization ? (
<RobotSettingButton
settingName={t('app_settings:language')}
settingInfo={
currentLanguageOption != null ? currentLanguageOption.name : ''
}
onClick={() => {
setCurrentOption('LanguageSetting')
}}
iconName="language"
/>
) : null}
<RobotSettingButton
settingName={t('display_led_lights')}
dataTestId="RobotSettingButton_display_led_lights"
Expand Down Expand Up @@ -225,8 +241,6 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element {
onClick={() => dispatch(toggleDevtools())}
/>
{devToolsOn ? <FeatureFlags /> : null}
{/* TODO(bh, 2024-09-23): remove when localization setting designs implemented */}
<LanguageToggle />
</Flex>
</Flex>
)
Expand Down Expand Up @@ -282,22 +296,3 @@ function FeatureFlags(): JSX.Element {
</>
)
}

function LanguageToggle(): JSX.Element | null {
const enableLocalization = useFeatureFlag('enableLocalization')
const dispatch = useDispatch<Dispatch>()

const { i18n } = useContext(I18nContext)

return enableLocalization ? (
<RobotSettingButton
settingName={`Change Language: ${i18n.language}`}
onClick={() => {
i18n.language === 'en'
? dispatch(updateConfigValue('language.appLanguage', 'zh'))
: dispatch(updateConfigValue('language.appLanguage', 'en'))
}}
rightElement={<></>}
/>
) : null
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest'
import { MemoryRouter } from 'react-router-dom'
import { fireEvent, screen } from '@testing-library/react'
import { when } from 'vitest-when'

import { renderWithProviders } from '/app/__testing-utils__'

import { i18n } from '/app/i18n'
import { getRobotSettings } from '/app/redux/robot-settings'
import { getLocalRobot } from '/app/redux/discovery'
import { toggleDevtools, toggleHistoricOffsets } from '/app/redux/config'
import {
getAppLanguage,
toggleDevtools,
toggleHistoricOffsets,
useFeatureFlag,
} from '/app/redux/config'
import { mockConnectedRobot } from '/app/redux/discovery/__fixtures__'
import { Navigation } from '/app/organisms/ODD/Navigation'
import {
DeviceReset,
TouchScreenSleep,
TouchscreenBrightness,
LanguageSetting,
NetworkSettings,
Privacy,
RobotSystemVersion,
Expand Down Expand Up @@ -44,6 +51,7 @@ vi.mock('/app/organisms/ODD/RobotSettingsDashboard/RobotSystemVersion')
vi.mock('/app/organisms/ODD/RobotSettingsDashboard/TouchscreenBrightness')
vi.mock('/app/organisms/ODD/RobotSettingsDashboard/UpdateChannel')
vi.mock('/app/organisms/ODD/RobotSettingsDashboard/Privacy')
vi.mock('/app/organisms/ODD/RobotSettingsDashboard/LanguageSetting')

const mockToggleLights = vi.fn()
const mockToggleER = vi.fn()
Expand All @@ -59,6 +67,8 @@ const render = () => {
)
}

const MOCK_DEFAULT_LANGUAGE = 'en-US'

// Note kj 01/25/2023 Currently test cases only check text since this PR is bare-bones for RobotSettings Dashboard
describe('RobotSettingsDashboard', () => {
beforeEach(() => {
Expand All @@ -81,6 +91,10 @@ describe('RobotSettingsDashboard', () => {
isEREnabled: true,
toggleERSettings: mockToggleER,
})
vi.mocked(getAppLanguage).mockReturnValue(MOCK_DEFAULT_LANGUAGE)
when(vi.mocked(useFeatureFlag))
.calledWith('enableLocalization')
.thenReturn(true)
})

afterEach(() => {
Expand Down Expand Up @@ -249,4 +263,13 @@ describe('RobotSettingsDashboard', () => {
render()
screen.getByText('Update available')
})

it('should render component when tapping Language', () => {
render()

screen.getByText('English (US)')
const button = screen.getByText('Language')
fireEvent.click(button)
expect(vi.mocked(LanguageSetting)).toHaveBeenCalled()
})
})
4 changes: 4 additions & 0 deletions app/src/pages/ODD/RobotSettingsDashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DeviceReset,
TouchscreenBrightness,
TouchScreenSleep,
LanguageSetting,
NetworkSettings,
Privacy,
RobotName,
Expand Down Expand Up @@ -200,6 +201,9 @@ export function RobotSettingsDashboard(): JSX.Element {
/>
)

case 'LanguageSetting':
return <LanguageSetting setCurrentOption={setCurrentOption} />

// fallthrough option: render the robot settings list of buttons
default:
return <RobotSettingsList setCurrentOption={setCurrentOption} />
Expand Down
10 changes: 9 additions & 1 deletion components/src/icons/icon-data.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// icon data
export const ICON_DATA_BY_NAME = {
export const ICON_DATA_BY_NAME: Record<
string,
{ path: string; viewBox: string }
> = {
add: {
path:
'M24 48C37.2548 48 48 37.2548 48 24C48 10.7452 37.2548 0 24 0C10.7452 0 0 10.7452 0 24C0 37.2548 10.7452 48 24 48ZM21 21V12H27V21H36V27H27V36H21V27H12V21H21Z',
Expand Down Expand Up @@ -272,6 +275,11 @@ export const ICON_DATA_BY_NAME = {
'M8.63355 14.215C8.6365 14.5124 8.63911 14.7764 8.63911 15H7.36528C7.36528 14.8447 7.36714 14.6621 7.36924 14.4568C7.38181 13.225 7.40273 11.1766 7.07922 9.31768C6.89019 8.23151 6.59339 7.27753 6.15429 6.60988C5.73178 5.96745 5.2075 5.625 4.50024 5.625C3.79297 5.625 3.2687 5.96745 2.84618 6.60988C2.40708 7.27753 2.11028 8.23151 1.92125 9.31768C1.59774 11.1766 1.61866 13.225 1.63124 14.4568C1.63333 14.6621 1.6352 14.8447 1.6352 15H0.385197C0.385197 14.8588 0.383339 14.6876 0.38121 14.4914C0.367987 13.273 0.344279 11.0886 0.689764 9.10337C0.890012 7.95271 1.2241 6.80142 1.80181 5.92302C2.39611 5.01939 3.27456 4.375 4.50024 4.375C5.72592 4.375 6.60437 5.01939 7.19867 5.92301C7.40983 6.24409 7.58845 6.60162 7.73987 6.98226C7.75025 6.91343 7.76092 6.84476 7.77188 6.77626C8.02647 5.18496 8.4487 3.62176 9.16765 2.44065C9.89976 1.23791 10.9823 0.375 12.5141 0.375C14.0321 0.375 15.1148 1.19161 15.852 2.35243C16.5736 3.48863 16.9954 4.99227 17.2488 6.52295C17.6344 8.85309 17.6513 11.4038 17.6372 13.1367L19.1208 12.0031L19.8797 12.9963L17.0169 15.1838L14.1235 12.9984L14.8769 12.001L16.3866 13.1413C16.4006 11.422 16.3863 8.96723 16.0155 6.72705C15.7724 5.25773 15.3851 3.94887 14.7968 3.02257C14.2242 2.12089 13.4961 1.625 12.5141 1.625C11.5458 1.625 10.8158 2.13709 10.2354 3.0906C9.64181 4.06574 9.25156 5.44004 9.00618 6.97374C8.58775 9.58904 8.61631 12.4739 8.63355 14.215Z',
viewBox: '0 0 20 16',
},
language: {
path:
'M10 18.3333C8.83335 18.3333 7.74308 18.1146 6.72919 17.6771C5.7153 17.2396 4.83335 16.6458 4.08335 15.8958C3.33335 15.1458 2.74308 14.2604 2.31252 13.2396C1.88196 12.2187 1.66669 11.125 1.66669 9.95832C1.66669 8.79166 1.88196 7.70485 2.31252 6.69791C2.74308 5.69096 3.33335 4.81249 4.08335 4.06249C4.83335 3.31249 5.7153 2.72568 6.72919 2.30207C7.74308 1.87846 8.83335 1.66666 10 1.66666C11.1667 1.66666 12.257 1.87846 13.2709 2.30207C14.2847 2.72568 15.1667 3.31249 15.9167 4.06249C16.6667 4.81249 17.257 5.69096 17.6875 6.69791C18.1181 7.70485 18.3334 8.79166 18.3334 9.95832C18.3334 11.125 18.1181 12.2187 17.6875 13.2396C17.257 14.2604 16.6667 15.1458 15.9167 15.8958C15.1667 16.6458 14.2847 17.2396 13.2709 17.6771C12.257 18.1146 11.1667 18.3333 10 18.3333ZM10 17.125C10.4861 16.625 10.8924 16.0521 11.2188 15.4062C11.5452 14.7604 11.8125 13.993 12.0209 13.1042H8.00002C8.19446 13.9375 8.45488 14.6875 8.78127 15.3542C9.10766 16.0208 9.51391 16.6111 10 17.125ZM8.22919 16.875C7.88196 16.3472 7.58335 15.7778 7.33335 15.1667C7.08335 14.5555 6.87502 13.868 6.70835 13.1042H3.58335C4.11113 14.0903 4.72224 14.8646 5.41669 15.4271C6.11113 15.9896 7.04863 16.4722 8.22919 16.875ZM11.7917 16.8542C12.7917 16.5347 13.691 16.0555 14.4896 15.4167C15.2882 14.7778 15.9306 14.0069 16.4167 13.1042H13.3125C13.132 13.8542 12.9202 14.5347 12.6771 15.1458C12.434 15.7569 12.1389 16.3264 11.7917 16.8542ZM3.16669 11.8542H6.47919C6.43752 11.4792 6.41321 11.1424 6.40627 10.8437C6.39933 10.5451 6.39585 10.25 6.39585 9.95832C6.39585 9.6111 6.4028 9.30207 6.41669 9.03124C6.43058 8.76041 6.45835 8.45832 6.50002 8.12499H3.16669C3.06946 8.45832 3.00349 8.75693 2.96877 9.02082C2.93405 9.28471 2.91669 9.59721 2.91669 9.95832C2.91669 10.3194 2.93405 10.6424 2.96877 10.9271C3.00349 11.2118 3.06946 11.5208 3.16669 11.8542ZM7.77085 11.8542H12.25C12.3056 11.4236 12.3403 11.0729 12.3542 10.8021C12.3681 10.5312 12.375 10.25 12.375 9.95832C12.375 9.68054 12.3681 9.41318 12.3542 9.15624C12.3403 8.89929 12.3056 8.55554 12.25 8.12499H7.77085C7.7153 8.55554 7.68058 8.89929 7.66669 9.15624C7.6528 9.41318 7.64585 9.68054 7.64585 9.95832C7.64585 10.25 7.6528 10.5312 7.66669 10.8021C7.68058 11.0729 7.7153 11.4236 7.77085 11.8542ZM13.5 11.8542H16.8334C16.9306 11.5208 16.9965 11.2118 17.0313 10.9271C17.066 10.6424 17.0834 10.3194 17.0834 9.95832C17.0834 9.59721 17.066 9.28471 17.0313 9.02082C16.9965 8.75693 16.9306 8.45832 16.8334 8.12499H13.5209C13.5625 8.6111 13.5903 8.98263 13.6042 9.23957C13.6181 9.49652 13.625 9.7361 13.625 9.95832C13.625 10.2639 13.6146 10.5521 13.5938 10.8229C13.5729 11.0937 13.5417 11.4375 13.5 11.8542ZM13.2917 6.87499H16.4167C15.9584 5.91666 15.3299 5.11805 14.5313 4.47916C13.7327 3.84027 12.8125 3.38888 11.7709 3.12499C12.1181 3.63888 12.4132 4.19443 12.6563 4.79166C12.8993 5.38888 13.1111 6.08332 13.2917 6.87499ZM8.00002 6.87499H12.0417C11.8889 6.13888 11.632 5.42707 11.2709 4.73957C10.9097 4.05207 10.4861 3.44443 10 2.91666C9.55558 3.29166 9.18058 3.78471 8.87502 4.39582C8.56946 5.00693 8.2778 5.83332 8.00002 6.87499ZM3.58335 6.87499H6.72919C6.88196 6.12499 7.07641 5.45485 7.31252 4.86457C7.54863 4.2743 7.84724 3.70138 8.20835 3.14582C7.16669 3.40971 6.25696 3.85416 5.47919 4.47916C4.70141 5.10416 4.06946 5.90277 3.58335 6.87499Z',
viewBox: '0 0 20 20',
},
'latch-closed': {
path:
'M33.6663 10H6.33301V17H10.333V14H14.167V19.166H26.667V14H29.6663V17H33.6663V10Z',
Expand Down

0 comments on commit d5e9932

Please sign in to comment.