diff --git a/package-lock.json b/package-lock.json index 3e3bad4ec..d1176692a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,6 @@ "dayjs": "1.11.5", "detect-browser": "5.3.0", "dotenv": "8.6.0", - "downshift": "6.1.12", "express": "4.18.1", "express-urlrewrite": "2.0.0", "html-react-parser": "1.4.12", @@ -7309,11 +7308,6 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, - "node_modules/compute-scroll-into-view": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz", - "integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -8637,26 +8631,6 @@ "node": ">=10" } }, - "node_modules/downshift": { - "version": "6.1.12", - "resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.12.tgz", - "integrity": "sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA==", - "dependencies": { - "@babel/runtime": "^7.14.8", - "compute-scroll-into-view": "^1.0.17", - "prop-types": "^15.7.2", - "react-is": "^17.0.2", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "react": ">=16.12.0" - } - }, - "node_modules/downshift/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -28121,11 +28095,6 @@ } } }, - "compute-scroll-into-view": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz", - "integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==" - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -29092,25 +29061,6 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" }, - "downshift": { - "version": "6.1.12", - "resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.12.tgz", - "integrity": "sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA==", - "requires": { - "@babel/runtime": "^7.14.8", - "compute-scroll-into-view": "^1.0.17", - "prop-types": "^15.7.2", - "react-is": "^17.0.2", - "tslib": "^2.3.0" - }, - "dependencies": { - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - } - } - }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", diff --git a/package.json b/package.json index 11400cf82..8c5d88921 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "dayjs": "1.11.5", "detect-browser": "5.3.0", "dotenv": "8.6.0", - "downshift": "6.1.12", "express": "4.18.1", "express-urlrewrite": "2.0.0", "html-react-parser": "1.4.12", diff --git a/src/komponenter/header/common/sprakvelger/SprakVelger.module.scss b/src/komponenter/header/common/sprakvelger/SprakVelger.module.scss index f21a6fb87..35f8e6dcf 100644 --- a/src/komponenter/header/common/sprakvelger/SprakVelger.module.scss +++ b/src/komponenter/header/common/sprakvelger/SprakVelger.module.scss @@ -11,17 +11,17 @@ } .container:only-child { - width: 100%; display: flex; flex-direction: row-reverse; + width: 100%; } .knapp { - padding: 0.5rem 0.5rem; - width: 100%; display: flex; flex-direction: row; justify-content: space-between; + padding: 0.5rem 0.5rem; + width: 100%; .chevronNed { align-self: center; @@ -37,24 +37,10 @@ } .ikon { - width: 20px; - height: 20px; filter: invert(50%); + height: 20px; margin-right: 1em; -} - -.option { - display: flex; - flex-direction: row; - align-items: center; - - svg { - margin-right: 0.1rem; - } - - .not-selected { - margin-left: 1.25rem; - } + width: 20px; } .sirkel { @@ -62,18 +48,30 @@ } .menu { + border: none; + box-sizing: border-box; + display: none; margin: 1px 0 0; - padding: 0; - z-index: 100; max-height: 10rem; - position: absolute; - overflow-y: auto; overflow-x: hidden; + overflow-y: auto; + padding: 0; + position: absolute; width: 100%; - box-sizing: border-box; + z-index: 100; } -.menuList { - padding: 0.5rem; + +.menuOpenState { + background-color: white; + border-radius: 0 0 4px 4px; + border-top: none; + border: 1px solid var(--a-gray-200); + box-shadow: 0 0.05rem 0.25rem 0.125rem rgba(0, 0, 0, 0.08); + display: block; + outline: none; +} + +.menuListItem { cursor: pointer; display: flex; border: 1px solid var(--a-gray-200); @@ -83,3 +81,30 @@ border-bottom: 0; } } + +.option { + align-items: center; + background-color: transparent; + border: 0; + cursor: pointer; + display: flex; + flex-direction: row; + margin: 0; + outline: none; + padding: 0.5rem; + width: 100%; + + svg { + margin-right: 0.1rem; + } + + .notSelected { + margin-left: 1.25rem; + } + + &:hover, + &:focus-visible { + background-color: var(--a-blue-600); + color: white; + } +} diff --git a/src/komponenter/header/common/sprakvelger/SprakVelger.tsx b/src/komponenter/header/common/sprakvelger/SprakVelger.tsx index 0154fd9bb..ede5341dd 100644 --- a/src/komponenter/header/common/sprakvelger/SprakVelger.tsx +++ b/src/komponenter/header/common/sprakvelger/SprakVelger.tsx @@ -1,9 +1,8 @@ -import React from 'react'; -import { BodyShort } from '@navikt/ds-react'; -import Globe from 'ikoner/globe.svg'; +import React, { useEffect } from 'react'; +import { BodyShort } from '@navikt/ds-react'; import { Expand } from '@navikt/ds-icons'; -import { useSelect } from 'downshift'; +import classNames from 'classnames'; import { decoratorLanguageCookie } from '../../Header'; import { cookieOptions } from '../../../../server/cookieSettings'; import { AvailableLanguage } from 'store/reducers/language-duck'; @@ -13,16 +12,10 @@ import { useCookies } from 'react-cookie'; import { useSelector, useStore } from 'react-redux'; import { AppState } from 'store/reducers'; import { Bilde } from '../../../common/bilde/Bilde'; +import Globe from 'ikoner/globe.svg'; import SprakVelgerItem from './SprakVelgerItem'; import style from 'komponenter/header/common/sprakvelger/SprakVelger.module.scss'; -export const farger = { - navGra20: '#C6C2BF', - navBla: '#0067C5', -}; - -const selectorLabel = 'Språk/Language'; - export type LocaleOption = AvailableLanguage & { label: string }; interface Props { @@ -33,73 +26,96 @@ export const SprakVelger = (props: Props) => { const store = useStore(); const availableLanguages = props.languages; const { language } = useSelector((state: AppState) => state.language); + const [isMenuOpen, setIsMenuOpen] = React.useState(false); + const [selectedLocale, setSelectedLocale] = React.useState(null); const [, setCookie] = useCookies([decoratorLanguageCookie]); - const options = transformOptions(availableLanguages).sort((a, b) => (a.label > b.label ? -1 : 1)); + const languageOptions = transformOptions(availableLanguages).sort((a, b) => (a.label > b.label ? -1 : 1)); + + useEffect(() => { + if (selectedLocale === null) { + const selected = languageOptions.find((option) => option.locale === language); + setSelectedLocale(selected || null); + } + }, []); const onChange = (selected: LocaleOption) => { const { label, ...selectedLanguage } = selected; - + setSelectedLocale(selected); setCookie(decoratorLanguageCookie, selectedLanguage.locale, cookieOptions); store.dispatch(languageDuck.actionCreator({ language: selectedLanguage.locale })); + if (selectedLanguage.handleInApp) { postMessageToApp('languageSelect', selectedLanguage); } else { window.location.assign(selectedLanguage.url); } + + toggleMenu(false); + }; + + const onKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Tab') { + const buttons = document.querySelectorAll(`.${style.sprakvelger} button`); + // If focus is not on any of the language option buttons, close the menu + const hasOptionFocus = Array.from(buttons).some((button) => button === document.activeElement); + if (!hasOptionFocus) { + toggleMenu(false); + } + } + + if (e.key === 'Escape') { + toggleMenu(false); + } + }; + + const onClick = (e: MouseEvent) => { + const isClickOutside = !e.composedPath().some((el) => el === document.querySelector(`.${style.sprakvelger}`)); + if (isClickOutside) { + toggleMenu(false); + } + }; + + const toggleMenu = (open?: boolean) => { + const isIntentToOpen = open !== undefined ? open : !isMenuOpen; + + if (isIntentToOpen) { + window.addEventListener('keyup', onKeyUp); + window.addEventListener('click', onClick); + } else { + window.removeEventListener('keyup', onKeyUp); + window.removeEventListener('click', onClick); + } + + setIsMenuOpen(isIntentToOpen); }; - const { isOpen, selectedItem, getToggleButtonProps, getMenuProps, highlightedIndex, getItemProps } = useSelect({ - items: options, - itemToString: (item) => item?.label || '', - onSelectedItemChange: ({ selectedItem }) => onChange(selectedItem as LocaleOption), - selectedItem: options.find((option) => option.locale === language), - }); - - const ulStyle = isOpen - ? { - boxShadow: '0 0.05rem 0.25rem 0.125rem rgba(0, 0, 0, 0.08)', - border: '1px solid', - borderRadius: '0 0 4px 4px', - outline: 'none', - borderColor: farger.navGra20, - borderTop: 'none', - } - : { - display: 'none', - border: 'none', - }; - // Adding aria-controls and removing aria-labelledby prop from downshift (to avoid an extra reading of label on screen readers) - let buttonProps = getToggleButtonProps(); - delete buttonProps['aria-labelledby']; - let menuProps = getMenuProps(); - delete menuProps['aria-labelledby']; - menuProps.id = 'sprakvelger_menuID'; - buttonProps['aria-controls'] = menuProps.id; return (
diff --git a/src/komponenter/header/common/sprakvelger/SprakVelgerItem.tsx b/src/komponenter/header/common/sprakvelger/SprakVelgerItem.tsx index c72440c6a..6c7e10047 100644 --- a/src/komponenter/header/common/sprakvelger/SprakVelgerItem.tsx +++ b/src/komponenter/header/common/sprakvelger/SprakVelgerItem.tsx @@ -1,48 +1,36 @@ import React from 'react'; -import Cicle from 'ikoner/circle.svg'; -import { farger, LocaleOption } from './SprakVelger'; +import { LocaleOption } from './SprakVelger'; import { Bilde } from '../../../common/bilde/Bilde'; import { BodyShort } from '@navikt/ds-react'; +import Cicle from 'ikoner/circle.svg'; import style from 'komponenter/header/common/sprakvelger/SprakVelger.module.scss'; interface Props { index: number; item: LocaleOption; - highlightedIndex: number; selectedItem: LocaleOption | null; - itemProps: any; + onSelectedItemChange: any; } const SprakVelgerItem = (props: Props) => { - const { selectedItem, highlightedIndex, index } = props; - const { item, itemProps } = props; + const { selectedItem, index, onSelectedItemChange } = props; + const { item } = props; + + const isItemSelected = selectedItem?.locale === item.locale; - const inlineStyle = - highlightedIndex === index - ? { - backgroundColor: farger.navBla, - color: 'white', - } - : { - backgroundColor: 'white', - color: 'black', - }; return ( -
  • - {selectedItem?.locale === item.locale ? ( -
    - - - {item.label} - -
    - ) : ( - - - {item.label} - +
  • +
  • ); }; diff --git a/src/server/template.tsx b/src/server/template.tsx index 7fbbb27a7..3141c22ff 100644 --- a/src/server/template.tsx +++ b/src/server/template.tsx @@ -56,6 +56,8 @@ export const template = (req: Request) => { const language = params.language || 'nb'; + const isExternallyAvailable = env.APP_URL.includes('www.nav.no'); + // Render SSR const HtmlHeader = ReactDOMServer.renderToString( @@ -99,9 +101,12 @@ export const template = (req: Request) => { display: flex; justify-content: center; align-items: center; - } - .decorator-utils-container { + } + ${ + isExternallyAvailable && + `.decorator-utils-container { display: none !important; + }` }