From c669aeb87c08f94e274cdf623ec5670865df5684 Mon Sep 17 00:00:00 2001 From: peter Date: Fri, 17 May 2024 12:43:31 +0200 Subject: [PATCH] feat(ffe-searchable-dropdown-react): rewrite in ts BREAKING CHANGE: innerRef -> ref. Som props maunall props where wrong --- .../exampleData.js | 243 ------------------ .../package.json | 5 +- .../src/{ListItemBody.js => ListItemBody.tsx} | 28 +- .../src/ListItemContainer.js | 44 ---- .../src/ListItemContainer.tsx | 51 ++++ .../src/NoMatch.js | 35 --- .../src/NoMatch.tsx | 41 +++ .../src/{Results.js => Results.tsx} | 66 ++--- ...wn.spec.js => SearchableDropdown.spec.tsx} | 205 +++++++++------ ...ableDropdown.js => SearchableDropdown.tsx} | 216 ++++++++++------ .../src/{a11y.js => a11y.ts} | 40 ++- .../src/fixedForwardRef.tsx | 7 + ...{getListToRender.js => getListToRender.ts} | 27 +- ...htedIndex.js => getNewHighlightedIndex.ts} | 10 +- .../src/index.d.ts | 44 ---- .../src/index.js | 1 - .../src/index.ts | 4 + .../src/mergeRefs.ts | 18 ++ .../src/propTypes.js | 91 ------- .../src/{reducer.js => reducer.ts} | 86 ++++--- .../{scrollIntoView.js => scrollIntoView.ts} | 9 +- .../src/{translations.js => translations.ts} | 68 ++--- .../src/types.ts | 22 ++ .../tsconfig.cjs.json | 9 + .../tsconfig.esm.json | 9 + .../tsconfig.types.json | 10 + packages/ffe-system-message-react/README.md | 5 - .../ffe-system-message-react/package.json | 10 +- ...ErrorMessage.js => SystemErrorMessage.tsx} | 45 +--- ...emInfoMessage.js => SystemInfoMessage.tsx} | 17 +- .../src/SystemMessage.spec.js | 110 -------- .../src/SystemMessage.spec.tsx | 120 +++++++++ .../{SystemMessage.js => SystemMessage.tsx} | 110 +++----- ...emNewsMessage.js => SystemNewsMessage.tsx} | 18 +- ...essMessage.js => SystemSuccessMessage.tsx} | 18 +- .../ffe-system-message-react/src/index.d.ts | 31 --- .../ffe-system-message-react/src/index.js | 11 - .../ffe-system-message-react/src/index.ts | 10 + .../src/{texts.js => texts.ts} | 8 +- .../tsconfig.cjs.json | 9 + .../tsconfig.esm.json | 9 + .../tsconfig.types.json | 10 + 42 files changed, 855 insertions(+), 1075 deletions(-) delete mode 100644 packages/ffe-searchable-dropdown-react/exampleData.js rename packages/ffe-searchable-dropdown-react/src/{ListItemBody.js => ListItemBody.tsx} (61%) delete mode 100644 packages/ffe-searchable-dropdown-react/src/ListItemContainer.js create mode 100644 packages/ffe-searchable-dropdown-react/src/ListItemContainer.tsx delete mode 100644 packages/ffe-searchable-dropdown-react/src/NoMatch.js create mode 100644 packages/ffe-searchable-dropdown-react/src/NoMatch.tsx rename packages/ffe-searchable-dropdown-react/src/{Results.js => Results.tsx} (60%) rename packages/ffe-searchable-dropdown-react/src/{SearchableDropdown.spec.js => SearchableDropdown.spec.tsx} (86%) rename packages/ffe-searchable-dropdown-react/src/{SearchableDropdown.js => SearchableDropdown.tsx} (65%) rename packages/ffe-searchable-dropdown-react/src/{a11y.js => a11y.ts} (77%) create mode 100644 packages/ffe-searchable-dropdown-react/src/fixedForwardRef.tsx rename packages/ffe-searchable-dropdown-react/src/{getListToRender.js => getListToRender.ts} (66%) rename packages/ffe-searchable-dropdown-react/src/{getNewHighlightedIndex.js => getNewHighlightedIndex.ts} (71%) delete mode 100644 packages/ffe-searchable-dropdown-react/src/index.d.ts delete mode 100644 packages/ffe-searchable-dropdown-react/src/index.js create mode 100644 packages/ffe-searchable-dropdown-react/src/index.ts create mode 100644 packages/ffe-searchable-dropdown-react/src/mergeRefs.ts delete mode 100644 packages/ffe-searchable-dropdown-react/src/propTypes.js rename packages/ffe-searchable-dropdown-react/src/{reducer.js => reducer.ts} (69%) rename packages/ffe-searchable-dropdown-react/src/{scrollIntoView.js => scrollIntoView.ts} (66%) rename packages/ffe-searchable-dropdown-react/src/{translations.js => translations.ts} (64%) create mode 100644 packages/ffe-searchable-dropdown-react/src/types.ts create mode 100644 packages/ffe-searchable-dropdown-react/tsconfig.cjs.json create mode 100644 packages/ffe-searchable-dropdown-react/tsconfig.esm.json create mode 100644 packages/ffe-searchable-dropdown-react/tsconfig.types.json rename packages/ffe-system-message-react/src/{SystemErrorMessage.js => SystemErrorMessage.tsx} (56%) rename packages/ffe-system-message-react/src/{SystemInfoMessage.js => SystemInfoMessage.tsx} (77%) delete mode 100644 packages/ffe-system-message-react/src/SystemMessage.spec.js create mode 100644 packages/ffe-system-message-react/src/SystemMessage.spec.tsx rename packages/ffe-system-message-react/src/{SystemMessage.js => SystemMessage.tsx} (52%) rename packages/ffe-system-message-react/src/{SystemNewsMessage.js => SystemNewsMessage.tsx} (77%) rename packages/ffe-system-message-react/src/{SystemSuccessMessage.js => SystemSuccessMessage.tsx} (74%) delete mode 100644 packages/ffe-system-message-react/src/index.d.ts delete mode 100644 packages/ffe-system-message-react/src/index.js create mode 100644 packages/ffe-system-message-react/src/index.ts rename packages/ffe-system-message-react/src/{texts.js => texts.ts} (91%) create mode 100644 packages/ffe-system-message-react/tsconfig.cjs.json create mode 100644 packages/ffe-system-message-react/tsconfig.esm.json create mode 100644 packages/ffe-system-message-react/tsconfig.types.json diff --git a/packages/ffe-searchable-dropdown-react/exampleData.js b/packages/ffe-searchable-dropdown-react/exampleData.js deleted file mode 100644 index 2077439193..0000000000 --- a/packages/ffe-searchable-dropdown-react/exampleData.js +++ /dev/null @@ -1,243 +0,0 @@ -const exports = (module.exports = {}); - -exports.companiesWithMessageCount = [ - { - organizationName: 'Bedriften', - organizationNumber: '912602370', - quantityUnprocessedMessages: 5, - }, - { - organizationName: 'Sønn & co', - organizationNumber: '812602372', - quantityUnprocessedMessages: 3, - }, - { - organizationName: 'Beslag skytter', - organizationNumber: '812602552', - quantityUnprocessedMessages: 1, - }, - { - organizationName: 'Rør og sånt', - organizationNumber: '812602399', - quantityUnprocessedMessages: 7, - }, - { - organizationName: 'Kaffekoppen', - organizationNumber: '812602222', - quantityUnprocessedMessages: 8, - }, - { - organizationName: 'Sats', - organizationNumber: '809602772', - quantityUnprocessedMessages: 2, - }, -]; - -exports.yrker = [ - { kode: '5', beskrivelse: 'Ukjent yrke oppgitt' }, - { kode: '6', beskrivelse: 'Kontor' }, - { kode: '7', beskrivelse: 'Service' }, - { kode: '8', beskrivelse: 'Fysisk arbeid' }, - { kode: '9', beskrivelse: '3D Animatør' }, - { kode: '10', beskrivelse: '3D Artist' }, - { kode: '11', beskrivelse: '3D Designer' }, - { kode: '12', beskrivelse: 'Abonnementselger' }, - { kode: '13', beskrivelse: 'Abonnementsjef' }, - { kode: '14', beskrivelse: 'Account manager' }, - { kode: '15', beskrivelse: 'Addiktolog' }, - { kode: '16', beskrivelse: 'Adjunkt' }, - { kode: '17', beskrivelse: 'Adjutant' }, - { kode: '18', beskrivelse: 'Administrasjonsdirektør' }, - { kode: '19', beskrivelse: 'Administrasjonskonsulent' }, - { kode: '20', beskrivelse: 'Administrasjonssekretær' }, - { kode: '21', beskrivelse: 'Administrasjonssjef' }, - { kode: '22', beskrivelse: 'Administrativ leder' }, - { kode: '23', beskrivelse: 'Administrerende direktør' }, - { kode: '24', beskrivelse: 'Admiral' }, - { kode: '25', beskrivelse: 'Advokat' }, - { kode: '26', beskrivelse: 'Advokatfullmektig' }, - { kode: '27', beskrivelse: 'Advokatsekretær' }, - { kode: '28', beskrivelse: 'Aerobicinstruktør' }, - { kode: '29', beskrivelse: 'Afis-fullmektig' }, - { kode: '30', beskrivelse: 'Agent' }, - { kode: '31', beskrivelse: 'Agronom' }, - { kode: '32', beskrivelse: 'Agrotekniker' }, - { kode: '33', beskrivelse: 'Airpurser' }, - { kode: '34', beskrivelse: 'Airsteward' }, - { kode: '35', beskrivelse: 'Akademiker' }, - { kode: '36', beskrivelse: 'Akrobat' }, - { kode: '37', beskrivelse: 'Aksjemegler' }, - { kode: '38', beskrivelse: 'Aktivitetsformidler' }, - { kode: '39', beskrivelse: 'Aktivitetskoordinator' }, - { kode: '40', beskrivelse: 'Aktivitør' }, - { kode: '41', beskrivelse: 'Aktmodell' }, - { kode: '42', beskrivelse: 'Aktuar' }, - { kode: '43', beskrivelse: 'Akupunktør' }, - { kode: '44', beskrivelse: 'Akvakulturarbeider' }, - { kode: '45', beskrivelse: 'Alarmmontør' }, - { kode: '46', beskrivelse: 'Alarmoperatør' }, - { kode: '47', beskrivelse: 'Alarmsentralleder' }, - { kode: '48', beskrivelse: 'Aldershjemsbestyrer' }, - { kode: '49', beskrivelse: 'Altmuligmann' }, - { kode: '50', beskrivelse: 'Amanuensis' }, - { kode: '51', beskrivelse: 'Ambassaderåd' }, - { kode: '52', beskrivelse: 'Ambassadesekretær' }, - { kode: '53', beskrivelse: 'Ambassadør' }, - { kode: '54', beskrivelse: 'Ambulansepersonell' }, - { kode: '55', beskrivelse: 'Analytiker' }, - { kode: '56', beskrivelse: 'Andrestyrmann' }, - { kode: '57', beskrivelse: 'Anestesilaborant' }, - { kode: '58', beskrivelse: 'Anestesilege' }, - { kode: '59', beskrivelse: 'Anestesisykepleier' }, - { kode: '60', beskrivelse: 'Animatør' }, - { kode: '61', beskrivelse: 'Anleggsarbeider' }, - { kode: '62', beskrivelse: 'Anleggsdykker' }, - { kode: '63', beskrivelse: 'Anleggsgartner' }, - { kode: '64', beskrivelse: 'Anleggsleder' }, - { kode: '65', beskrivelse: 'Anleggsmaskinfører' }, - { kode: '66', beskrivelse: 'Anleggsmaskinmekaniker' }, - { kode: '67', beskrivelse: 'Anleggsmaskinreparatør' }, - { kode: '68', beskrivelse: 'Anleggsmekaniker' }, - { kode: '69', beskrivelse: 'Anleggsoperatør' }, - { kode: '70', beskrivelse: 'Annonsekonsulent' }, - { kode: '71', beskrivelse: 'Annonsesjef' }, - { kode: '72', beskrivelse: 'Antenneteknikker' }, - { kode: '73', beskrivelse: 'Antikvar' }, - { kode: '74', beskrivelse: 'Apoteker' }, - { kode: '75', beskrivelse: 'Apotekmedarbeider' }, - { kode: '76', beskrivelse: 'Apotektekniker' }, - { kode: '77', beskrivelse: 'Arbeidsavklaringspenger' }, - { kode: '78', beskrivelse: 'Arbeidsdirektør' }, - { kode: '79', beskrivelse: 'Arbeidsformidler' }, - { kode: '80', beskrivelse: 'Arbeidsledig' }, - { kode: '81', beskrivelse: 'Arbeidsmiljøsjef' }, - { kode: '82', beskrivelse: 'Arbeidspsykolog' }, - { kode: '83', beskrivelse: 'Arbeidssøker' }, - { kode: '84', beskrivelse: 'Area manager' }, - { kode: '85', beskrivelse: 'Area sales manager' }, - { kode: '86', beskrivelse: 'Arealplanlegger' }, - { kode: '87', beskrivelse: 'Arkeolog' }, - { kode: '88', beskrivelse: 'Arkitekt' }, - { kode: '89', beskrivelse: 'Arkivar' }, - { kode: '90', beskrivelse: 'Arkivmedarbeider' }, - { kode: '91', beskrivelse: 'Arkivsjef' }, - { kode: '92', beskrivelse: 'Aromaterapeut' }, - { kode: '93', beskrivelse: 'Arrangementssjef' }, - { kode: '94', beskrivelse: 'Arrestforvarer' }, - { kode: '95', beskrivelse: 'Art director' }, - { kode: '96', beskrivelse: 'Asfaltarbeider' }, - { kode: '97', beskrivelse: 'Assistent sfo' }, - { kode: '98', beskrivelse: 'Assistent skole' }, - { kode: '99', beskrivelse: 'Assistentlærer' }, - { kode: '100', beskrivelse: 'Assisterende banksjef' }, - { kode: '101', beskrivelse: 'Assisterende borer' }, - { kode: '102', beskrivelse: 'Assisterende butikksjef' }, - { kode: '103', beskrivelse: 'Assisterende byfogd' }, - { kode: '104', beskrivelse: 'Assisterende direktør' }, - { kode: '105', beskrivelse: 'Assisterende etatsjef' }, - { kode: '106', beskrivelse: 'Assisterende forvaltningssjef' }, - { kode: '107', beskrivelse: 'Assisterende fylkesarbeidssjef' }, - { kode: '108', beskrivelse: 'Assisterende fylkeshelsesjef' }, - { kode: '109', beskrivelse: 'Assisterende fylkeslege' }, - { kode: '110', beskrivelse: 'Assisterende fylkesmann' }, - { kode: '111', beskrivelse: 'Assisterende fylkesrådmann' }, - { kode: '112', beskrivelse: 'Assisterende fylkessykehussjef' }, - { kode: '113', beskrivelse: 'Assisterende fylkestannlege' }, - { kode: '114', beskrivelse: 'Assisterende kjøkkensjef' }, - { kode: '115', beskrivelse: 'Assisterende kringkastingssjef' }, - { kode: '116', beskrivelse: 'Assisterende overjordmor' }, - { kode: '117', beskrivelse: 'Assisterende overlege' }, - { kode: '118', beskrivelse: 'Assisterende riksadvokat' }, - { kode: '119', beskrivelse: 'Assisterende rådmann' }, - { kode: '120', beskrivelse: 'Assisterende sjefflygeleder' }, - { kode: '121', beskrivelse: 'Assisterende sjefspsykolog' }, - { kode: '122', beskrivelse: 'Assisterende sjefstråleterapeut' }, - { kode: '123', beskrivelse: 'Assisterende sjefsykepleier' }, - { kode: '124', beskrivelse: 'Assisterende sysselmann' }, - { kode: '125', beskrivelse: 'Assurandør' }, - { kode: '126', beskrivelse: 'Astrofysiker' }, - { kode: '127', beskrivelse: 'Astrolog' }, - { kode: '128', beskrivelse: 'Astronom' }, - { kode: '129', beskrivelse: 'Atomfysiker' }, - { kode: '130', beskrivelse: 'Attache' }, - { kode: '131', beskrivelse: 'Attføringskonsulent' }, - { kode: '132', beskrivelse: 'Audiograf' }, - { kode: '133', beskrivelse: 'Audiometriassistent' }, - { kode: '134', beskrivelse: 'Audiopedagog' }, - { kode: '135', beskrivelse: 'Auksjonarius' }, - { kode: '136', beskrivelse: 'Au-Pair' }, - { kode: '137', beskrivelse: 'Automatiker' }, - { kode: '138', beskrivelse: 'Automatikkmekaniker' }, - { kode: '139', beskrivelse: 'Automatiseringsmontør' }, - { kode: '140', beskrivelse: 'Automatiseringstekniker' }, - { kode: '141', beskrivelse: 'Autorisert finansiell rådgiver' }, - { kode: '142', beskrivelse: 'Autorisert regnskapsfører' }, - { kode: '143', beskrivelse: 'Avdelingsdirektør' }, - { kode: '144', beskrivelse: 'Avdelingsleder' }, - { kode: '145', beskrivelse: 'Avdelingsoverlege' }, - { kode: '146', beskrivelse: 'Avdelingspsykolog' }, - { kode: '147', beskrivelse: 'Avdelingssjef' }, - { kode: '148', beskrivelse: 'Avisbud' }, - { kode: '149', beskrivelse: 'Avløser' }, - { kode: '150', beskrivelse: 'Badevakt' }, - { kode: '151', beskrivelse: 'Baker' }, - { kode: '152', beskrivelse: 'Bakkemannskap' }, - { kode: '153', beskrivelse: 'Bakteriolog' }, - { kode: '154', beskrivelse: 'Ballettdanser' }, - { kode: '155', beskrivelse: 'Ballettpedagog' }, - { kode: '156', beskrivelse: 'Bandasjist' }, - { kode: '157', beskrivelse: 'Banearbeider' }, - { kode: '158', beskrivelse: 'Banemester' }, - { kode: '159', beskrivelse: 'Banemontør' }, - { kode: '160', beskrivelse: 'Baneregionsjef' }, - { kode: '161', beskrivelse: 'Banereperatør' }, - { kode: '162', beskrivelse: 'Baneservicesjef' }, - { kode: '163', beskrivelse: 'Banesjef' }, - { kode: '164', beskrivelse: 'Bankanalytiker' }, - { kode: '165', beskrivelse: 'Bankassistent' }, - { kode: '166', beskrivelse: 'Bankdirektør' }, - { kode: '167', beskrivelse: 'Bankfunksjonær' }, - { kode: '168', beskrivelse: 'Bankkasserer' }, - { kode: '169', beskrivelse: 'Bankkonsulent' }, - { kode: '170', beskrivelse: 'Bankrådgiver' }, - { kode: '171', beskrivelse: 'Banksjef' }, - { kode: '172', beskrivelse: 'Barista' }, - { kode: '173', beskrivelse: 'Barkeeper' }, - { kode: '174', beskrivelse: 'Barne- og ungdomsarbeider' }, - { kode: '175', beskrivelse: 'Barnehageassistent' }, - { kode: '176', beskrivelse: 'Barnehagekonsulent' }, - { kode: '177', beskrivelse: 'Barnehagelærer' }, - { kode: '178', beskrivelse: 'Barnehagestyrer' }, - { kode: '179', beskrivelse: 'Barnehjemsbestyrer' }, - { kode: '180', beskrivelse: 'Barnelege' }, - { kode: '181', beskrivelse: 'Barneombud' }, - { kode: '182', beskrivelse: 'Barneparkleder' }, - { kode: '183', beskrivelse: 'Barnepleier' }, - { kode: '184', beskrivelse: 'Barnevernskonsulent' }, - { kode: '185', beskrivelse: 'Barnevernskurator' }, - { kode: '186', beskrivelse: 'Barnevernspedagog' }, - { kode: '187', beskrivelse: 'Barnevernssjef' }, - { kode: '188', beskrivelse: 'Bartender' }, - { kode: '189', beskrivelse: 'Baseoperatør' }, - { kode: '190', beskrivelse: 'Basketballtrener' }, - { kode: '191', beskrivelse: 'Bedriftsrådgiver' }, - { kode: '192', beskrivelse: 'Bedriftsøkonom' }, - { kode: '193', beskrivelse: 'Befal' }, - { kode: '194', beskrivelse: 'Befrakter' }, - { kode: '195', beskrivelse: 'Befraktningssjef' }, - { kode: '196', beskrivelse: 'Begravelsesbyråarbeider' }, - { kode: '197', beskrivelse: 'Bemanningskoordinator' }, - { kode: '198', beskrivelse: 'Bensinstasjon medarbeider ' }, - { kode: '199', beskrivelse: 'Bergfagarbeider' }, - { kode: '200', beskrivelse: 'Bergmester' }, - { kode: '201', beskrivelse: 'Bergtekniker' }, - { kode: '202', beskrivelse: 'Bergverksarbeider' }, - { kode: '203', beskrivelse: 'Betongarbeider' }, - { kode: '204', beskrivelse: 'Betongpumpeoperatør' }, -]; - -const listWithThirtyThousandElements = new Array(30000) - .fill(null) - .map((_, i) => ({ organizationName: `Navn ${i}` })); - -exports.listWithThirtyThousandElements = listWithThirtyThousandElements; diff --git a/packages/ffe-searchable-dropdown-react/package.json b/packages/ffe-searchable-dropdown-react/package.json index 5b781350e9..a66fea9e87 100644 --- a/packages/ffe-searchable-dropdown-react/package.json +++ b/packages/ffe-searchable-dropdown-react/package.json @@ -19,7 +19,7 @@ "url": "ssh://git@github.com:SpareBank1/designsystem.git" }, "scripts": { - "build": "ffe-buildtool babel", + "build": "ffe-buildtool tsc", "watch": "ffe-buildtool babel-watch", "lint": "eslint src", "lint:fix": "eslint src --fix", @@ -32,10 +32,9 @@ "@sb1/ffe-icons-react": "^10.0.1", "@sb1/ffe-spinner-react": "^8.0.2", "classnames": "^2.3.1", - "compute-scroll-into-view": "^1.0.17", + "compute-scroll-into-view": "^3.1.0", "lodash.debounce": "^4.0.8", "lodash.isequal": "^4.5.0", - "prop-types": "^15.7.2", "react-custom-scrollbars-2": "^4.3.0", "uuid": "^9.0.0" }, diff --git a/packages/ffe-searchable-dropdown-react/src/ListItemBody.js b/packages/ffe-searchable-dropdown-react/src/ListItemBody.tsx similarity index 61% rename from packages/ffe-searchable-dropdown-react/src/ListItemBody.js rename to packages/ffe-searchable-dropdown-react/src/ListItemBody.tsx index d77bc2b9f1..ac02bdf1da 100644 --- a/packages/ffe-searchable-dropdown-react/src/ListItemBody.js +++ b/packages/ffe-searchable-dropdown-react/src/ListItemBody.tsx @@ -1,14 +1,22 @@ import React from 'react'; -import { object, arrayOf, string, bool } from 'prop-types'; import classnames from 'classnames'; - import { MicroText } from '@sb1/ffe-core-react'; -const ListItemBody = ({ item, dropdownAttributes, isHighlighted }) => { +interface ListItemBodyProps> { + item: Item; + dropdownAttributes: Array; + isHighlighted: boolean; +} + +export function ListItemBody>({ + item, + dropdownAttributes, + isHighlighted, +}: ListItemBodyProps) { const [titleAttribute, ...restAttributes] = dropdownAttributes; const title = item[titleAttribute]; - const rest = restAttributes.map(attribute => ( - {item[attribute]} + const rest = restAttributes.map((attribute, index) => ( + {item[attribute]} )); return (
{ )}
); -}; - -ListItemBody.propTypes = { - item: object.isRequired, - dropdownAttributes: arrayOf(string).isRequired, - isHighlighted: bool.isRequired, -}; - -export default ListItemBody; +} diff --git a/packages/ffe-searchable-dropdown-react/src/ListItemContainer.js b/packages/ffe-searchable-dropdown-react/src/ListItemContainer.js deleted file mode 100644 index 16bf4a0751..0000000000 --- a/packages/ffe-searchable-dropdown-react/src/ListItemContainer.js +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useRef } from 'react'; -import { any, bool, func, object, oneOfType, shape } from 'prop-types'; -import { v4 as uuid } from 'uuid'; - -const ListItemContainer = ({ - item, - isHighlighted, - children, - forwardedRef, - onMouseEnter, - onClick, -}) => { - const id = useRef(`ìtem-${uuid()}`); - return ( - // eslint-disable-next-line jsx-a11y/interactive-supports-focus -
- {children({ - item, - isHighlighted, - })} -
- ); -}; - -ListItemContainer.propTypes = { - item: object.isRequired, - isHighlighted: bool.isRequired, - children: func.isRequired, - forwardedRef: oneOfType([func, shape({ current: any })]), - onMouseEnter: func, - onClick: func, -}; - -export default React.forwardRef((props, ref) => { - return ; -}); diff --git a/packages/ffe-searchable-dropdown-react/src/ListItemContainer.tsx b/packages/ffe-searchable-dropdown-react/src/ListItemContainer.tsx new file mode 100644 index 0000000000..86e1086982 --- /dev/null +++ b/packages/ffe-searchable-dropdown-react/src/ListItemContainer.tsx @@ -0,0 +1,51 @@ +import React, { useRef, ForwardedRef } from 'react'; +import { v4 as uuid } from 'uuid'; +import { fixedForwardRef } from './fixedForwardRef'; + +interface ListItemContainerProps> { + item: Item; + isHighlighted: boolean; + children: ({ + item, + isHighlighted, + }: { + item: Item; + isHighlighted: boolean; + }) => React.ReactNode; + onClick: React.MouseEventHandler; + onMouseEnter?: React.MouseEventHandler; +} + +function ListItemContainerWithForwardRef>( + { + item, + isHighlighted, + children, + onMouseEnter, + onClick, + }: ListItemContainerProps, + ref: ForwardedRef, +) { + const id = useRef(`ìtem-${uuid()}`).current; + return ( + // eslint-disable-next-line jsx-a11y/interactive-supports-focus +
+ {children({ + item, + isHighlighted, + })} +
+ ); +} + +export const ListItemContainer = fixedForwardRef( + ListItemContainerWithForwardRef, +); diff --git a/packages/ffe-searchable-dropdown-react/src/NoMatch.js b/packages/ffe-searchable-dropdown-react/src/NoMatch.js deleted file mode 100644 index b52d24ce99..0000000000 --- a/packages/ffe-searchable-dropdown-react/src/NoMatch.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { arrayOf, object, shape, string } from 'prop-types'; - -import { Paragraph } from '@sb1/ffe-core-react'; -import { getNotMatchText } from './translations'; - -const NoMatch = ({ noMatch, noMatchMessageId, listToRender }) => ( -
- {noMatch.text ? ( -
- {noMatch.text} -
- ) : ( - listToRender.length === 0 && ( - - {getNotMatchText()} - - ) - )} -
-); - -NoMatch.propTypes = { - noMatch: shape({ - text: string, - dropdownList: arrayOf(object), - }), - noMatchMessageId: string, - listToRender: arrayOf(object).isRequired, -}; - -export default NoMatch; diff --git a/packages/ffe-searchable-dropdown-react/src/NoMatch.tsx b/packages/ffe-searchable-dropdown-react/src/NoMatch.tsx new file mode 100644 index 0000000000..65d58d00fb --- /dev/null +++ b/packages/ffe-searchable-dropdown-react/src/NoMatch.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +import { Paragraph } from '@sb1/ffe-core-react'; +import { getNotMatchText } from './translations'; +import { Locale } from './types'; + +interface NoMatchProps> { + noMatch: { + text?: string; + dropdownList?: Item[]; + }; + noMatchMessageId: string; + listToRender: Item[]; + locale: Locale; +} + +export function NoMatch>({ + noMatch, + noMatchMessageId, + listToRender, + locale, +}: NoMatchProps) { + return ( +
+ {noMatch.text ? ( +
+ {noMatch.text} +
+ ) : ( + listToRender.length === 0 && ( + + {getNotMatchText(locale)} + + ) + )} +
+ ); +} diff --git a/packages/ffe-searchable-dropdown-react/src/Results.js b/packages/ffe-searchable-dropdown-react/src/Results.tsx similarity index 60% rename from packages/ffe-searchable-dropdown-react/src/Results.js rename to packages/ffe-searchable-dropdown-react/src/Results.tsx index 22411e8ba8..c5fdd91a1f 100644 --- a/packages/ffe-searchable-dropdown-react/src/Results.js +++ b/packages/ffe-searchable-dropdown-react/src/Results.tsx @@ -1,23 +1,30 @@ import React from 'react'; import { Scrollbars } from 'react-custom-scrollbars-2'; -import { - arrayOf, - any, - bool, - func, - number, - object, - oneOf, - shape, - string, -} from 'prop-types'; +import { ListItemContainer } from './ListItemContainer'; +import { NoMatch } from './NoMatch'; +import { Locale } from './types'; -import { locales } from './translations'; -import ListItemContainer from './ListItemContainer'; -import NoMatch from './NoMatch'; +interface ResultProps> { + noMatch?: { + text?: string; + dropdownList?: Item[]; + }; + listToRender: Item[]; + noMatchMessageId: string; + ListItemBodyElement: React.ComponentType<{ + item: Item; + isHighlighted: boolean; + dropdownAttributes: (keyof Item)[]; + locale: Locale; + }>; + highlightedIndex?: number; + refs: React.Ref[]; + dropdownAttributes: (keyof Item)[]; + locale: Locale; + onChange: (item: Item) => void; +} -const Results = ({ - isNoMatch, +export function Results>({ noMatch, listToRender, noMatchMessageId, @@ -27,14 +34,15 @@ const Results = ({ dropdownAttributes, locale, onChange, -}) => { +}: ResultProps) { return ( - {isNoMatch && ( + {noMatch && ( )} {listToRender.map((item, index) => ( @@ -50,30 +58,12 @@ const Results = ({ {props => ( )}
))} ); -}; - -Results.propTypes = { - listToRender: arrayOf(object).isRequired, - noMatch: shape({ - text: string, - dropdownList: arrayOf(object), - }), - noMatchMessageId: string, - ListItemBodyElement: func, - highlightedIndex: number, - dropdownAttributes: arrayOf(string).isRequired, - locale: oneOf(Object.values(locales)).isRequired, - refs: arrayOf(any).isRequired, - onChange: func.isRequired, - isNoMatch: bool.isRequired, -}; - -export default Results; +} diff --git a/packages/ffe-searchable-dropdown-react/src/SearchableDropdown.spec.js b/packages/ffe-searchable-dropdown-react/src/SearchableDropdown.spec.tsx similarity index 86% rename from packages/ffe-searchable-dropdown-react/src/SearchableDropdown.spec.js rename to packages/ffe-searchable-dropdown-react/src/SearchableDropdown.spec.tsx index eaba77fee1..2d7837391a 100644 --- a/packages/ffe-searchable-dropdown-react/src/SearchableDropdown.spec.js +++ b/packages/ffe-searchable-dropdown-react/src/SearchableDropdown.spec.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { render, screen, act, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import 'regenerator-runtime'; import { SearchableDropdown } from './SearchableDropdown'; describe('SearchableDropdown', () => { @@ -29,7 +28,7 @@ describe('SearchableDropdown', () => { render( { render( { await user.type(input, 'Be'); - await user.click(screen.getByText('Bedriften'), { button: 1 }); + await user.click(screen.getByText('Bedriften')); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(companies[0]); - expect(input.value).toEqual('Bedriften'); + expect(input.getAttribute('value')).toEqual('Bedriften'); }); it('should be possible to select item with keyboard', async () => { @@ -92,7 +91,7 @@ describe('SearchableDropdown', () => { render( { expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(companies[1]); - expect(input.value).toEqual('Sønn & co'); + expect(input.getAttribute('value')).toEqual('Sønn & co'); }); it('should show "noMatch" values', async () => { @@ -133,7 +132,7 @@ describe('SearchableDropdown', () => { render( { it('should render custom elements', async () => { const user = userEvent.setup(); - /* eslint-disable react/prop-types */ - const CustomListItemBody = ({ item, isHighlighted }) => { + const CustomListItemBody = ({ + item, + isHighlighted, + }: { + item: (typeof companies)[number]; + isHighlighted: boolean; + }) => { return (
{
); }; - /* eslint-enable react/prop-types */ const onChange = jest.fn(); render( { render( { name: /åpne alternativer/i, }); - await user.click(openButton, { button: 1 }); + await user.click(openButton); expect(screen.getByText('Bedriften')).toBeInTheDocument(); expect(screen.getByText('912602370')).toBeInTheDocument(); @@ -260,7 +263,7 @@ describe('SearchableDropdown', () => { name: /lukk alternativer/i, }); - await user.click(closeButton, { button: 1 }); + await user.click(closeButton); expect(screen.queryByText('Bedriften')).toBeNull(); expect(screen.queryByText('912602370')).toBeNull(); @@ -275,7 +278,7 @@ describe('SearchableDropdown', () => { render( { render( { }); it('should set a11y status message briefly on element change', async () => { - const user = userEvent.setup(); + const user = userEvent.setup({ delay: null }); + jest.useFakeTimers(); render(
{ const input = await screen.findByRole('combobox'); - await user.click(input); - await user.type(input, '{arrowdown}'); - await user.type(input, '{enter}'); + await act(async () => { + await user.click(input); + await user.type(input, '{arrowdown}'); + await user.type(input, '{enter}'); + }); const a11yStatusMessage = await screen.findByRole('status'); @@ -391,8 +397,10 @@ describe('SearchableDropdown', () => { expect(a11yStatusMessage).toHaveTextContent(''); }); - await user.clear(input); - await user.click(screen.getByText('Knapp')); + await act(async () => { + await user.clear(input); + await user.click(screen.getByText('Knapp')); + }); await waitFor(() => { expect(a11yStatusMessage).toHaveTextContent( @@ -402,16 +410,18 @@ describe('SearchableDropdown', () => { await waitFor(() => { expect(a11yStatusMessage).toHaveTextContent(''); }); + jest.useRealTimers(); }); it('should set a11y status message briefly on state change', async () => { - const user = userEvent.setup(); + const user = userEvent.setup({ delay: null }); + jest.useFakeTimers(); render(
{ const input = await screen.findByRole('combobox'); - await user.click(input); + await act(async () => { + await user.click(input); + }); const a11yStatusMessage = await screen.findByRole('status'); @@ -441,7 +453,9 @@ describe('SearchableDropdown', () => { expect(a11yStatusMessage).toHaveTextContent(''); }); - await user.type(input, 'be'); + await act(async () => { + await user.type(input, 'be'); + }); await waitFor(() => { expect(a11yStatusMessage).toHaveTextContent( @@ -452,8 +466,10 @@ describe('SearchableDropdown', () => { expect(a11yStatusMessage).toHaveTextContent(''); }); - await user.clear(input); - await user.type(input, 'ingen'); + await act(async () => { + await user.clear(input); + await user.type(input, 'ingen'); + }); await waitFor(() => { expect(a11yStatusMessage).toHaveTextContent( @@ -463,9 +479,12 @@ describe('SearchableDropdown', () => { await waitFor(() => { expect(a11yStatusMessage).toHaveTextContent(''); }); + await act(async () => { + await user.click(screen.getByText('Knapp')); + }); - await user.click(screen.getByText('Knapp')); expect(a11yStatusMessage).toHaveTextContent(''); + jest.useRealTimers(); }); it('should highlight the first element when typing', async () => { @@ -474,7 +493,7 @@ describe('SearchableDropdown', () => { const { container } = render( { { const input = screen.getByRole('combobox'); - await user.type(input, 'Be'); + await act(async () => { + await user.type(input, 'Be'); - await user.click(screen.getByText('Bedriften'), { button: 1 }); + await user.click(screen.getByText('Bedriften')); - await user.clear(input); + await user.clear(input); - await user.click(screen.getByText('Knapp')); + await user.click(screen.getByText('Knapp')); + }); expect(onChange).toHaveBeenCalledTimes(2); expect(onChange).toHaveBeenCalledWith(null); - expect(input.value).toEqual(''); + expect(input.getAttribute('value')).toEqual(''); }); it('should reset the input field value to the saved selected element when pressing Escape', async () => { @@ -541,7 +562,7 @@ describe('SearchableDropdown', () => { render( { await user.type(input, 'Be'); - await user.click(screen.getByText('Bedriften'), { button: 1 }); + await user.click(screen.getByText('Bedriften')); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(companies[0]); - expect(input.value).toEqual('Bedriften'); + expect(input.getAttribute('value')).toEqual('Bedriften'); await user.clear(input); await user.type(input, 'B'); - expect(input.value).toEqual('B'); + expect(input.getAttribute('value')).toEqual('B'); await user.keyboard('{Esc}'); - expect(input.value).toEqual('Bedriften'); + expect(input.getAttribute('value')).toEqual('Bedriften'); }); it('should move focus to toggle button when selecting from dropdown', async () => { @@ -575,7 +596,7 @@ describe('SearchableDropdown', () => { render( { const input = screen.getByRole('combobox'); await user.click(input); - await user.click(screen.getByText('Bedriften'), { button: 1 }); + await user.click(screen.getByText('Bedriften')); const toggleButton = screen.getByLabelText('åpne alternativer'); expect(document.activeElement).toEqual(toggleButton); @@ -601,7 +622,7 @@ describe('SearchableDropdown', () => { { const input = screen.getByRole('combobox'); - await user.clear(input); - await user.type(input, 'Besla'); - expect(input.value).toEqual('Besla'); + await act(async () => { + await user.clear(input); + await user.type(input, 'Besla'); + }); - await user.click(screen.getByText('Knapp')); - expect(input.value).toEqual('Beslag skytter'); + expect(input.getAttribute('value')).toEqual('Besla'); + await act(async () => { + await user.click(screen.getByText('Knapp')); + }); + + expect(input.getAttribute('value')).toEqual('Beslag skytter'); }); it('should format input value when passing formatter', async () => { @@ -634,14 +660,14 @@ describe('SearchableDropdown', () => { const formatter = jest.fn(text => { return text .split('') - .map(char => `${char}!?_`) + .map((char: string) => `${char}!?_`) .join(''); }); render( { const input = screen.getByRole('combobox'); await user.type(input, 'H'); - expect(input.value).toEqual('H!?_'); + expect(input.getAttribute('value')).toEqual('H!?_'); }); it('allows using a custom search matcher', async () => { const onChange = jest.fn(); const user = userEvent.setup(); - const cleanString = value => `${value}`.replace(/h/g, 'sky'); + const cleanString = (value: string | number) => + `${value}`.replace(/h/g, 'sky'); const searchMatcher = jest.fn( - (inputValue, searchAttributes) => item => { - const cleanedInputValue = cleanString(inputValue); - return searchAttributes - .map(searchAttribute => cleanString(item[searchAttribute])) - .some(cleanedItemAttribute => - cleanedItemAttribute.includes(cleanedInputValue), - ); - }, + (inputValue, searchAttributes) => + (item: (typeof companies)[number]) => { + const cleanedInputValue = cleanString(inputValue); + return searchAttributes + .map( + ( + searchAttribute: keyof (typeof companies)[number], + ) => cleanString(item[searchAttribute]), + ) + .some((cleanedItemAttribute: string) => + cleanedItemAttribute.includes(cleanedInputValue), + ); + }, ); render( { const input = screen.getByRole('combobox'); await user.type(input, 'Beslag htter'); - expect(input.value).toEqual('Beslag htter'); + expect(input.getAttribute('value')).toEqual('Beslag htter'); expect(screen.queryByText('Bedriften')).toBeNull(); expect(screen.queryByText('912602370')).toBeNull(); @@ -704,7 +736,7 @@ describe('SearchableDropdown', () => { const { rerender } = render( { const input = screen.getByRole('combobox'); - expect(input.value).toBe(''); + expect(input.getAttribute('value')).toBe(''); rerender( { />, ); - expect(input.value).toBe('Beslag skytter'); + expect(input.getAttribute('value')).toBe('Beslag skytter'); }); it('allows for writing and selecting even when passing selectedItem', async () => { @@ -740,7 +772,7 @@ describe('SearchableDropdown', () => { render( { const input = screen.getByRole('combobox'); - expect(input.value).toBe('Sønn & co'); + expect(input.getAttribute('value')).toBe('Sønn & co'); await user.clear(input); await user.type(input, 'Be'); - await user.click(screen.getByText('Beslag skytter'), { button: 1 }); + await user.click(screen.getByText('Beslag skytter')); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(companies[2]); - expect(input.value).toEqual('Beslag skytter'); + expect(input.getAttribute('value')).toEqual('Beslag skytter'); }); it('should not automatically change selectedItem if object structure is different from previous but the actual content is still the same content', async () => { @@ -773,7 +805,7 @@ describe('SearchableDropdown', () => { { ); const input = screen.getByRole('combobox'); - await user.click(input); - await user.click(screen.getByText('Knapp')); + await act(async () => { + await user.click(input); + await user.click(screen.getByText('Knapp')); + }); + expect(onChange).not.toHaveBeenCalled(); }); @@ -817,7 +852,7 @@ describe('SearchableDropdown', () => { { const input = screen.getByRole('combobox'); await user.click(input); - act(async () => { + await act(async () => { await user.click(screen.getByText('Knapp')); }); expect(onChange).not.toHaveBeenCalled(); @@ -855,7 +890,7 @@ describe('SearchableDropdown', () => { { ); const input = screen.getByRole('combobox'); - act(async () => { + await act(async () => { await user.click(screen.getByText('Knapp')); }); expect(onChange).not.toHaveBeenCalled(); - expect(input.value).toEqual(''); + expect(input.getAttribute('value')).toEqual(''); }); it('should show updated result list after loading completes', async () => { @@ -888,7 +923,7 @@ describe('SearchableDropdown', () => { { { { value, - searchMatcher, - selectedItem, - isLoading = false, - onOpen, - onClose, - results: ResultsElement = Results, - innerRef, -}) => { +export interface SearchableDropdownProps> { + /** Id of drop down */ + id: string; + /** Id of element that labels input field */ + labelledById?: string; + /** Extra class */ + className?: string; + /** List of objects to be displayed in dropdown */ + dropdownList: Item[]; + /** The selected item to be displayed in the input field. If not specified, uses internal state to decide. */ + selectedItem?: Item | null; + /** Array of attributes to be displayed in list */ + dropdownAttributes: (keyof Item)[]; + /** Array of attributes used when filtering search */ + searchAttributes: (keyof Item)[]; + /** Props used on input field */ + inputProps?: React.ComponentProps<'input'>; + /** Limits number of rendered dropdown elements */ + maxRenderedDropdownElements?: number; + /** Called when a value is selected */ + onChange?: (item: Item | null) => void; + /** Custom element to use for each item in dropDownList */ + listElementBody?: React.ComponentType<{ + item: Item; + isHighlighted: boolean; + dropdownAttributes: (keyof Item)[]; + locale: Locale; + }>; + /** Element to be shown below dropDownList */ + postListElement?: React.ReactNode; + /** Message and a dropdownList to use when no match */ + noMatch?: { + text?: string; + dropdownList?: Item[]; + }; + /** Locale to use for translations */ + locale?: Locale; + /** aria-invalid attribute */ + ariaInvalid?: React.ComponentProps<'input'>['aria-invalid']; + /** Function used to format the input field value */ + formatter?: (value: string) => string; + /** + * Function used to decide if an item matches the input field value + * (inputValue: string, searchAttributes: string[]) => (item) => boolean + */ + searchMatcher?: SearchMatcher; + /** + * For situations where the dropdownList prop will be updated at a later point in time. + * That is, if the consumer first sends down an initial value before sending down data + * that has loaded. + */ + isLoading?: boolean; + /** Function used when dropdown opens */ + onOpen?: () => void; + /** Function used when dropdown closes */ + onClose?: () => void; +} + +function SearchableDropdownWithForwardRef>( + { + id, + labelledById, + className, + dropdownList, + dropdownAttributes, + searchAttributes, + maxRenderedDropdownElements = Number.MAX_SAFE_INTEGER, + onChange, + inputProps, + listElementBody: CustomListItemBody, + postListElement, + noMatch, + locale = 'nb', + ariaInvalid, + formatter = value => value, + searchMatcher, + selectedItem, + isLoading = false, + onOpen, + onClose, + }: SearchableDropdownProps, + ref: ForwardedRef, +) { const [state, dispatch] = useReducer( createReducer({ dropdownList, searchAttributes, maxRenderedDropdownElements, - noMatchDropdownList: noMatch.dropdownList, + noMatchDropdownList: noMatch?.dropdownList, searchMatcher, onChange, }), @@ -76,39 +138,38 @@ export const SearchableDropdown = ({ searchAttributes, maxRenderedDropdownElements, dropdownList, - noMatchDropdownList: noMatch.dropdownList, + noMatchDropdownList: noMatch?.dropdownList, searchMatcher, showAllItemsInDropdown: !!selectedItem, }), }; }, ); - const [refs, setRefs] = useState([]); + const [refs, setRefs] = useState[]>([]); const [hasFocus, setHasFocus] = useState(false); - const internalRef = useRef(); - const inputRef = innerRef || internalRef; - const toggleButtonRef = useRef(); - const containerRef = useRef(); + const inputRef = useRef(null); + const toggleButtonRef = useRef(null); + const containerRef = useRef(null); const ListItemBodyElement = CustomListItemBody || ListItemBody; - const listBoxRef = useRef(uuid()); + const listBoxRef = useRef(null); const noMatchMessageId = useRef(uuid()); const shouldFocusToggleButton = useRef(false); const shouldFocusInput = useRef(false); const handleInputClick = () => { - dispatch({ type: stateChangeTypes.InputClick }); + dispatch({ type: 'InputClick' }); }; - const handleInputBlur = e => { - if (inputProps.onBlur) { + const handleInputBlur = (e: React.FocusEvent) => { + if (inputProps?.onBlur) { inputProps.onBlur(e); } }; useEffect(() => { dispatch({ - type: stateChangeTypes.ItemSelectedProgrammatically, + type: 'ItemSelectedProgrammatically', payload: { selectedItem }, }); }, [selectedItem, dispatch]); @@ -133,17 +194,17 @@ export const SearchableDropdown = ({ useLayoutEffect(() => { if (shouldFocusToggleButton.current) { - toggleButtonRef.current.focus(); + toggleButtonRef.current?.focus(); shouldFocusToggleButton.current = false; } else if (shouldFocusInput.current) { - inputRef.current.focus(); + inputRef.current?.focus(); shouldFocusInput.current = false; } }); useEffect(() => { dispatch({ - type: stateChangeTypes.DropdownListPropUpdated, + type: 'DropdownListPropUpdated', }); }, [dropdownList, dispatch]); @@ -160,14 +221,16 @@ export const SearchableDropdown = ({ * event flag will only work in react v17. Therefore, we also check Element.contains() * to keep react v16 compatibility. */ - const handleContainerFocus = e => { + const handleContainerFocus = (e: MouseEvent | FocusEvent) => { const isFocusInside = - containerRef.current.contains(e.target) || + // @ts-ignore + containerRef.current?.contains(e.target) || + // @ts-ignore e.__eventFromFFESearchableDropdownId === id; if (!isFocusInside) { dispatch({ - type: stateChangeTypes.FocusMovedOutSide, + type: 'FocusMovedOutSide', }); } }; @@ -185,24 +248,25 @@ export const SearchableDropdown = ({ * can determine whether or not this event originated from this * component */ - function addFlagOnEventHandler(event) { + function addFlagOnEventHandler(event: React.SyntheticEvent) { + // @ts-ignore // eslint-disable-next-line no-param-reassign event.nativeEvent.__eventFromFFESearchableDropdownId = id; } - const handleKeyDown = event => { + const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === ENTER && state.highlightedIndex >= 0) { event.preventDefault(); dispatch({ - type: stateChangeTypes.InputKeyDownEnter, + type: 'InputKeyDownEnter', payload: { selectedItem: state.listToRender[state.highlightedIndex], }, }); - onChange(state.listToRender[state.highlightedIndex]); + onChange?.(state.listToRender[state.highlightedIndex]); return; } else if (event.key === ESCAPE) { - dispatch({ type: stateChangeTypes.InputKeyDownEscape }); + dispatch({ type: 'InputKeyDownEscape' }); return; } @@ -214,7 +278,7 @@ export const SearchableDropdown = ({ state.listToRender.length, ); dispatch({ - type: stateChangeTypes.InputKeyDownArrowUp, + type: 'InputKeyDownArrowUp', payload: { highlightedIndex: newHighlightedIndex }, }); scrollIntoView( @@ -232,7 +296,7 @@ export const SearchableDropdown = ({ state.listToRender.length, ); dispatch({ - type: stateChangeTypes.InputKeyDownArrowDown, + type: 'InputKeyDownArrowDown', payload: { highlightedIndex: newHighlightedIndex }, }); scrollIntoView( @@ -254,17 +318,17 @@ export const SearchableDropdown = ({
{ - if (inputProps.onChange) { + if (inputProps?.onChange) { inputProps.onChange(e); } dispatch({ - type: stateChangeTypes.InputChange, + type: 'InputChange', payload: { inputValue: e.target.value }, }); }} @@ -272,17 +336,19 @@ export const SearchableDropdown = ({ onBlur={handleInputBlur} aria-describedby={ [ - inputProps['aria-describedby'], + inputProps?.['aria-describedby'], state.noMatch && noMatchMessageId.current, ] .filter(Boolean) - .join(' ') || null + .join(' ') || undefined } value={formatter(state.inputValue)} type="text" role="combobox" autoComplete="off" - aria-controls={listBoxRef.current} + aria-controls={ + listBoxRef.current?.getAttribute('id') ?? undefined + } aria-expanded={ state.isExpanded && !!state.listToRender.length } @@ -292,14 +358,10 @@ export const SearchableDropdown = ({ state.highlightedIndex >= 0 ? refs[ state.highlightedIndex - ]?.current?.getAttribute('id') - : null - } - aria-invalid={ - typeof ariaInvalid === 'string' - ? ariaInvalid - : String(!!ariaInvalid) + ]?.current?.getAttribute('id') ?? undefined + : undefined } + aria-invalid={ariaInvalid} />
-
+
{state.isExpanded && ( - { shouldFocusToggleButton.current = true; dispatch({ - type: stateChangeTypes.ItemOnClick, + type: 'ItemOnClick', payload: { selectedItem: item }, }); - onChange(item); + onChange?.(item); }} - isNoMatch={state.noMatch} - noMatch={noMatch} + noMatch={state.noMatch ? noMatch : undefined} noMatchMessageId={noMatchMessageId.current} /> )} @@ -368,6 +428,8 @@ export const SearchableDropdown = ({
); -}; +} -SearchableDropdown.propTypes = propTypes; +export const SearchableDropdown = fixedForwardRef( + SearchableDropdownWithForwardRef, +); diff --git a/packages/ffe-searchable-dropdown-react/src/a11y.js b/packages/ffe-searchable-dropdown-react/src/a11y.ts similarity index 77% rename from packages/ffe-searchable-dropdown-react/src/a11y.js rename to packages/ffe-searchable-dropdown-react/src/a11y.ts index b512448324..d00e079549 100644 --- a/packages/ffe-searchable-dropdown-react/src/a11y.js +++ b/packages/ffe-searchable-dropdown-react/src/a11y.ts @@ -7,6 +7,7 @@ import { getResultCountChangedA11yStatus, } from './translations'; import debounce from 'lodash.debounce'; +import { Locale } from './types'; const getStatusDiv = () => { const id = 'a11y-status-message'; @@ -38,7 +39,7 @@ const cleanupStatus = debounce(() => { getStatusDiv().textContent = ''; }, 500); -const setStatus = status => { +const setStatus = (status: string) => { const div = getStatusDiv(); if (!status) { return; @@ -52,14 +53,28 @@ const updateA11yStatus = debounce(getA11yMessage => { setStatus(getA11yMessage()); }, 200); -const getItemSelectedMessage = ({ selectedItemValue, locale }) => { +const getItemSelectedMessage = ({ + selectedItemValue, + locale, +}: { + selectedItemValue: string | undefined; + locale: Locale; +}) => { if (!selectedItemValue) { return getItemClearedA11yStatus(locale); } return getItemSelectedA11yStatus(locale, selectedItemValue); }; -const getStateChangeMessage = ({ isExpanded, resultCount, locale }) => { +const getStateChangeMessage = ({ + isExpanded, + resultCount, + locale, +}: { + isExpanded: boolean; + resultCount: number; + locale: Locale; +}) => { if (!isExpanded) { return ''; } @@ -71,11 +86,13 @@ const getStateChangeMessage = ({ isExpanded, resultCount, locale }) => { return getResultCountChangedA11yStatus(locale, resultCount); }; -const getIsLoadingItemsMessage = locale => { +const getIsLoadingItemsMessage = (locale: Locale) => { return getIsLoadingItemsA11yStatus(locale); }; -export const useSetAllyMessageItemSelection = ({ +export const useSetAllyMessageItemSelection = < + Item extends Record, +>({ hasFocus, isExpanded, isLoading, @@ -83,10 +100,19 @@ export const useSetAllyMessageItemSelection = ({ resultCount, searchAttributes, selectedItem, +}: { + hasFocus: boolean; + isExpanded: boolean; + isLoading: boolean; + locale: Locale; + resultCount: number; + searchAttributes: Array; + selectedItem: Item | null | undefined; }) => { const isInitialMount = useRef(true); - const prevSelectedItemValue = useRef(null); - const selectedItemValue = selectedItem?.[searchAttributes[0]]; + const prevSelectedItemValue = useRef(); + const selectedItemValue: string | undefined = + selectedItem?.[searchAttributes[0]]; useEffect(() => { if (isLoading && hasFocus) { diff --git a/packages/ffe-searchable-dropdown-react/src/fixedForwardRef.tsx b/packages/ffe-searchable-dropdown-react/src/fixedForwardRef.tsx new file mode 100644 index 0000000000..68bc15fd3c --- /dev/null +++ b/packages/ffe-searchable-dropdown-react/src/fixedForwardRef.tsx @@ -0,0 +1,7 @@ +import React, { forwardRef } from 'react'; + +type FixedForwardRef = ( + render: (props: P, ref: React.Ref) => React.JSX.Element, +) => (props: P & React.RefAttributes) => React.JSX.Element; + +export const fixedForwardRef = forwardRef as FixedForwardRef; diff --git a/packages/ffe-searchable-dropdown-react/src/getListToRender.js b/packages/ffe-searchable-dropdown-react/src/getListToRender.ts similarity index 66% rename from packages/ffe-searchable-dropdown-react/src/getListToRender.js rename to packages/ffe-searchable-dropdown-react/src/getListToRender.ts index 23201b0c3e..20470967f5 100644 --- a/packages/ffe-searchable-dropdown-react/src/getListToRender.js +++ b/packages/ffe-searchable-dropdown-react/src/getListToRender.ts @@ -1,14 +1,15 @@ -const toLowerCase = value => `${value}`.toLowerCase(); +import { SearchMatcher } from './types'; +const toLowerCase = (value: string) => `${value}`.toLowerCase(); -const filterDropdownList = ( - dropdownList, - searchAttributes, - inputValue, - searchMatcher, +const filterDropdownList = >( + dropdownList: Item[], + searchAttributes: Array, + inputValue: string, + searchMatcher?: SearchMatcher, ) => { const filter = searchMatcher ? searchMatcher(inputValue, searchAttributes) - : item => + : (item: Item) => searchAttributes .map(searchAttribute => toLowerCase(item[searchAttribute])) .some(cleanItemAttribute => @@ -17,7 +18,7 @@ const filterDropdownList = ( return dropdownList.filter(filter); }; -export const getListToRender = ({ +export const getListToRender = >({ inputValue, searchAttributes, maxRenderedDropdownElements, @@ -25,7 +26,15 @@ export const getListToRender = ({ noMatchDropdownList, searchMatcher, showAllItemsInDropdown, -}) => { +}: { + inputValue: string; + searchAttributes: Array; + maxRenderedDropdownElements: number; + dropdownList: Item[]; + noMatchDropdownList: Item[] | undefined; + searchMatcher?: SearchMatcher; + showAllItemsInDropdown: boolean; +}): { noMatch: boolean; listToRender: Item[] } => { const trimmedInput = inputValue ? inputValue.trim() : ''; const shouldFilter = trimmedInput.length > 0; diff --git a/packages/ffe-searchable-dropdown-react/src/getNewHighlightedIndex.js b/packages/ffe-searchable-dropdown-react/src/getNewHighlightedIndex.ts similarity index 71% rename from packages/ffe-searchable-dropdown-react/src/getNewHighlightedIndex.js rename to packages/ffe-searchable-dropdown-react/src/getNewHighlightedIndex.ts index cb6b044c04..2b827805b4 100644 --- a/packages/ffe-searchable-dropdown-react/src/getNewHighlightedIndex.js +++ b/packages/ffe-searchable-dropdown-react/src/getNewHighlightedIndex.ts @@ -1,4 +1,7 @@ -export const getNewHighlightedIndexUp = (currentIndex, numberOfElements) => { +export const getNewHighlightedIndexUp = ( + currentIndex: number, + numberOfElements: number, +) => { /** * When nothing is highlighted currentIndex is -1. * When pressing arrow key up and either the first item is selected or no item is selected we wrap around to the last item. @@ -9,7 +12,10 @@ export const getNewHighlightedIndexUp = (currentIndex, numberOfElements) => { return currentIndex - 1; }; -export const getNewHighlightedIndexDown = (currentIndex, numberOfElements) => { +export const getNewHighlightedIndexDown = ( + currentIndex: number, + numberOfElements: number, +) => { /** * When pressing arrow key down and the last item is selected we wrap around to the first item. */ diff --git a/packages/ffe-searchable-dropdown-react/src/index.d.ts b/packages/ffe-searchable-dropdown-react/src/index.d.ts deleted file mode 100644 index 77b315215d..0000000000 --- a/packages/ffe-searchable-dropdown-react/src/index.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as React from 'react'; - -interface ListElementBodyProps { - item: T; - isHighlighted: boolean; -} - -interface NoMatch { - text?: string; - dropdownList?: T[]; -} - -export interface SearchableDropdownProps { - id: string; - labelledById?: string; - className?: string; - dropdownList: T[]; - dropdownAttributes: (keyof T)[]; - searchAttributes: (keyof T)[]; - inputProps?: React.ComponentProps<'input'>; - selectedItem?: T; - maxRenderedDropdownElements?: number; - onChange?: (dropdownListItem: T) => any; - listElementBody?: (props: ListElementBodyProps) => React.ReactNode; - postListElement?: React.ReactNode; - noMatch?: NoMatch; - locale: 'nn' | 'nb' | 'en'; - ariaInvalid?: 'true' | 'false' | boolean; - formatter?: (value: string) => string; - searchMatcher?: ( - inputValue: string, - searchAttributes: (keyof T)[], - ) => (item: T) => boolean; - isLoading?: boolean; - onOpen?: () => void; - onClose?: () => void; - innerRef?: React.Ref; -} - -declare class SearchableDropdown extends React.Component< - SearchableDropdownProps -> {} - -export { SearchableDropdown }; diff --git a/packages/ffe-searchable-dropdown-react/src/index.js b/packages/ffe-searchable-dropdown-react/src/index.js deleted file mode 100644 index 59204aca29..0000000000 --- a/packages/ffe-searchable-dropdown-react/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { SearchableDropdown } from './SearchableDropdown'; diff --git a/packages/ffe-searchable-dropdown-react/src/index.ts b/packages/ffe-searchable-dropdown-react/src/index.ts new file mode 100644 index 0000000000..2e3e94f04a --- /dev/null +++ b/packages/ffe-searchable-dropdown-react/src/index.ts @@ -0,0 +1,4 @@ +export { + SearchableDropdown, + SearchableDropdownProps, +} from './SearchableDropdown'; diff --git a/packages/ffe-searchable-dropdown-react/src/mergeRefs.ts b/packages/ffe-searchable-dropdown-react/src/mergeRefs.ts new file mode 100644 index 0000000000..8b4c0eb32a --- /dev/null +++ b/packages/ffe-searchable-dropdown-react/src/mergeRefs.ts @@ -0,0 +1,18 @@ +import React from 'react'; + +export function mergeRefs( + refs: Array< + React.MutableRefObject | React.LegacyRef | undefined | null + >, +): React.RefCallback { + return value => { + refs.forEach(ref => { + if (typeof ref === 'function') { + ref(value); + } else if (ref != null) { + // eslint-disable-next-line no-param-reassign + (ref as React.MutableRefObject).current = value; + } + }); + }; +} diff --git a/packages/ffe-searchable-dropdown-react/src/propTypes.js b/packages/ffe-searchable-dropdown-react/src/propTypes.js deleted file mode 100644 index cf80bfe77e..0000000000 --- a/packages/ffe-searchable-dropdown-react/src/propTypes.js +++ /dev/null @@ -1,91 +0,0 @@ -import { locales } from './translations'; -import { - arrayOf, - node, - bool, - func, - number, - object, - oneOf, - oneOfType, - shape, - string, - element, -} from 'prop-types'; - -export const propTypes = { - /** Id of drop down */ - id: string.isRequired, - - /** Id of element that labels input field */ - labelledById: string, - - /** Extra class */ - className: string, - - /** List of objects to be displayed in dropdown */ - dropdownList: arrayOf(object).isRequired, - - /** The selected item to be displayed in the input field. If not specified, uses internal state to decide. */ - selectedItem: object, - - /** Array of attributes to be displayed in list */ - dropdownAttributes: arrayOf(string).isRequired, - - /** Array of attributes used when filtering search */ - searchAttributes: arrayOf(string).isRequired, - - /** Props used on input field */ - inputProps: shape({ - onFocus: func, - }), - - /** Limits number of rendered dropdown elements */ - maxRenderedDropdownElements: number, - - /** Called when a value is selected */ - onChange: func, - - /** Custom element to use for each item in dropDownList */ - listElementBody: func, - - /** Element to be shown below dropDownList */ - postListElement: node, - - /** Message and a dropdownList to use when no match */ - noMatch: shape({ - text: string, - dropdownList: arrayOf(object), - }), - - /** Locale to use for translations */ - locale: oneOf(Object.values(locales)), - - /** aria-invalid attribute */ - ariaInvalid: oneOfType([string, bool]), - - /** Function used to format the input field value */ - formatter: func, - - /** - * Function used to decide if an item matches the input field value - * (inputValue: string, searchAttributes: string[]) => (item) => boolean - */ - searchMatcher: func, - - /** - * For situations where the dropdownList prop will be updated at a later point in time. - * That is, if the consumer first sends down an initial value before sending down data - * that has loaded. - */ - isLoading: bool, - - /** Function used when dropdown opens */ - onOpen: func, - - /** Function used when dropdown closes */ - onClose: func, - - /** For internal usage */ - results: element, -}; diff --git a/packages/ffe-searchable-dropdown-react/src/reducer.js b/packages/ffe-searchable-dropdown-react/src/reducer.ts similarity index 69% rename from packages/ffe-searchable-dropdown-react/src/reducer.js rename to packages/ffe-searchable-dropdown-react/src/reducer.ts index c341937006..64ab84e90d 100644 --- a/packages/ffe-searchable-dropdown-react/src/reducer.js +++ b/packages/ffe-searchable-dropdown-react/src/reducer.ts @@ -1,33 +1,44 @@ import { getListToRender } from './getListToRender'; +import { StateChange, SearchMatcher } from './types'; -export const stateChangeTypes = { - InputFocus: 'InputFocus', - InputBlur: 'InputBlur', - InputClick: 'InputClick', - InputChange: 'InputChange', - InputKeyDownEscape: 'InputKeyDownEscape', - InputKeyDownEnter: 'InputKeyDownEnter', - InputKeyDownArrowDown: 'InputKeyDownArrowDown', - InputKeyDownArrowUp: 'InputKeyDownArrowUp', - ToggleButtonPressed: 'ToggleButtonPressed', - ItemOnClick: 'ItemOnMouseDown', - FocusMovedOutSide: 'FocusMovedOutSide', - ItemSelectedProgrammatically: 'ItemSelectedProgrammatically', - DropdownListPropUpdated: 'DropdownListPropUpdated', +type Action> = { + type: StateChange; + payload?: { + inputValue?: string; + selectedItem?: Item | null | undefined; + highlightedIndex?: number; + }; +}; + +type State> = { + noMatch: boolean; + isExpanded: boolean; + highlightedIndex: number; + selectedItem: Item | null | undefined; + inputValue: string; + listToRender: Item[]; + noMatchDropdownList?: Item[] | undefined; }; export const createReducer = - ({ + >({ searchAttributes, dropdownList, noMatchDropdownList, maxRenderedDropdownElements, searchMatcher, onChange, + }: { + dropdownList: Item[]; + searchAttributes: Array; + noMatchDropdownList: Item[] | undefined; + maxRenderedDropdownElements: number; + searchMatcher: SearchMatcher | undefined; + onChange: ((item: Item | null) => void) | undefined; }) => - (state, action) => { + (state: State, action: Action): State => { switch (action.type) { - case stateChangeTypes.InputKeyDownEscape: + case 'InputKeyDownEscape': return { ...state, noMatch: false, @@ -37,7 +48,7 @@ export const createReducer = ? state.selectedItem[searchAttributes[0]] : '', }; - case stateChangeTypes.InputClick: { + case 'InputClick': { const { noMatch, listToRender } = getListToRender({ inputValue: state.inputValue, searchAttributes, @@ -55,9 +66,9 @@ export const createReducer = noMatch, }; } - case stateChangeTypes.InputChange: { + case 'InputChange': { const { noMatch, listToRender } = getListToRender({ - inputValue: action.payload.inputValue, + inputValue: action.payload?.inputValue ?? '', searchAttributes, maxRenderedDropdownElements, dropdownList, @@ -69,44 +80,44 @@ export const createReducer = return { ...state, isExpanded: true, - inputValue: action.payload.inputValue, + inputValue: action.payload?.inputValue ?? '', listToRender, highlightedIndex: - action.payload.inputValue.trim() === '' || + action.payload?.inputValue?.trim() === '' || listToRender.length === 0 ? -1 : 0, noMatch, }; } - case stateChangeTypes.ToggleButtonPressed: + case 'ToggleButtonPressed': return { ...state, isExpanded: !state.isExpanded, }; - case stateChangeTypes.ItemSelectedProgrammatically: - case stateChangeTypes.ItemOnClick: - case stateChangeTypes.InputKeyDownEnter: + case 'ItemSelectedProgrammatically': + case 'ItemOnClick': + case 'InputKeyDownEnter': return { ...state, isExpanded: false, highlightedIndex: -1, - selectedItem: action.payload.selectedItem, + selectedItem: action.payload?.selectedItem, inputValue: - action.payload.selectedItem?.[searchAttributes[0]] || + action.payload?.selectedItem?.[searchAttributes[0]] || '', }; - case stateChangeTypes.InputKeyDownArrowDown: - case stateChangeTypes.InputKeyDownArrowUp: { + case 'InputKeyDownArrowDown': + case 'InputKeyDownArrowUp': { return { ...state, isExpanded: true, - highlightedIndex: action.payload.highlightedIndex, + highlightedIndex: action.payload?.highlightedIndex ?? -1, }; } - case stateChangeTypes.FocusMovedOutSide: { + case 'FocusMovedOutSide': { const { listToRender } = getListToRender({ inputValue: state.inputValue, searchAttributes, @@ -120,7 +131,7 @@ export const createReducer = const shouldEmptySelectedItem = state.inputValue === '' && !!state.selectedItem; - const shouldAutomaticallySetSelectedItem = !!( + const shouldAutomaticallySetSelectedItem = state.listToRender.length === 1 && searchAttributes .map( @@ -129,16 +140,15 @@ export const createReducer = state.selectedItem?.[searchAttribute], ) .includes(false) && - state.highlightedIndex !== -1 - ); + state.highlightedIndex !== -1; let selectedItem = state.selectedItem; if (shouldEmptySelectedItem) { - onChange(null); + onChange?.(null); selectedItem = null; } else if (shouldAutomaticallySetSelectedItem) { - onChange(state.listToRender[0]); + onChange?.(state.listToRender[0]); selectedItem = state.listToRender[0]; } @@ -154,7 +164,7 @@ export const createReducer = listToRender, }; } - case stateChangeTypes.DropdownListPropUpdated: { + case 'DropdownListPropUpdated': { return { ...state, ...getListToRender({ diff --git a/packages/ffe-searchable-dropdown-react/src/scrollIntoView.js b/packages/ffe-searchable-dropdown-react/src/scrollIntoView.ts similarity index 66% rename from packages/ffe-searchable-dropdown-react/src/scrollIntoView.js rename to packages/ffe-searchable-dropdown-react/src/scrollIntoView.ts index 19519ee4b6..7cb4c2ac28 100644 --- a/packages/ffe-searchable-dropdown-react/src/scrollIntoView.js +++ b/packages/ffe-searchable-dropdown-react/src/scrollIntoView.ts @@ -1,11 +1,14 @@ -import computeScrollIntoView from 'compute-scroll-into-view'; +import { compute } from 'compute-scroll-into-view'; -export const scrollIntoView = (node, menuNode) => { +export const scrollIntoView = ( + node: Element | null, + menuNode: Element | null, +) => { if (!node) { return; } - const actions = computeScrollIntoView(node, { + const actions = compute(node, { boundary: menuNode, block: 'nearest', scrollMode: 'if-needed', diff --git a/packages/ffe-searchable-dropdown-react/src/translations.js b/packages/ffe-searchable-dropdown-react/src/translations.ts similarity index 64% rename from packages/ffe-searchable-dropdown-react/src/translations.js rename to packages/ffe-searchable-dropdown-react/src/translations.ts index a685caa0ba..e24dedbb6d 100644 --- a/packages/ffe-searchable-dropdown-react/src/translations.js +++ b/packages/ffe-searchable-dropdown-react/src/translations.ts @@ -1,91 +1,79 @@ -const nb = 'nb'; -const nn = 'nn'; -const en = 'en'; +import { Locale } from './types'; -export const locales = { nb, nn, en }; - -export const getButtonLabelClear = locale => { - switch (locale) { - case nn: - return 'fjern valgt'; - case en: - return 'clear the field'; - default: - return 'fjern valgt'; - } -}; - -export const getButtonLabelClose = locale => { +export const getButtonLabelClose = (locale: Locale) => { switch (locale) { - case nn: + case 'nn': return 'lukk alternativer'; - case en: + case 'en': return 'close alternatives'; default: return 'lukk alternativer'; } }; -export const getButtonLabelOpen = locale => { +export const getButtonLabelOpen = (locale: Locale) => { switch (locale) { - case nn: + case 'nn': return 'åpne alternativer'; - case en: + case 'en': return 'open alternatives'; default: return 'åpne alternativer'; } }; -export const getNotMatchText = locale => { +export const getNotMatchText = (locale: Locale) => { switch (locale) { - case nn: + case 'nn': return 'Søket gav ingen treff'; - case en: + case 'en': return 'The search gave no result'; default: return 'Søket ga ingen treff'; } }; -export const getItemClearedA11yStatus = locale => { +export const getItemClearedA11yStatus = (locale: Locale) => { switch (locale) { - case nn: + case 'nn': return `Valt element har vorte fjerna.`; - case en: + case 'en': return `Selected item has been removed.`; default: return `Valgt element har blitt fjernet.`; } }; -export const getItemSelectedA11yStatus = (locale, item) => { +export const getItemSelectedA11yStatus = (locale: Locale, item: string) => { switch (locale) { - case nn: + case 'nn': return `Element ${item} er valgt.`; - case en: + case 'en': return `Item ${item} has been selected.`; default: return `Element ${item} er valgt.`; } }; -export const getNoResultA11yStatus = locale => { +export const getNoResultA11yStatus = (locale: Locale) => { switch (locale) { - case nn: + case 'nn': return 'Ingen resultat er tilgjengelege.'; - case en: + case 'en': return 'No results are available.'; default: return 'Ingen resultater er tilgjengelige.'; } }; -export const getResultCountChangedA11yStatus = (locale, nrOfItems) => { +export const getResultCountChangedA11yStatus = ( + locale: Locale, + nrOfItems: number, +) => { switch (locale) { - case nn: + case 'nn': return `${nrOfItems} resultat er tilgjengeleg, bruk opp- og nedpiltastene for å navigera. Trykk Enter for å velja.`; - case en: + case 'en': return `${nrOfItems} result${ nrOfItems === 1 ? ' is' : 's are' } available, use up and down arrow keys to navigate. Press Enter key to select.`; @@ -96,11 +84,11 @@ export const getResultCountChangedA11yStatus = (locale, nrOfItems) => { } }; -export const getIsLoadingItemsA11yStatus = locale => { +export const getIsLoadingItemsA11yStatus = (locale: Locale) => { switch (locale) { - case nn: + case 'nn': return 'Lastar inn alternativ.'; - case en: + case 'en': return 'Loading options.'; default: return 'Laster inn alternativer.'; diff --git a/packages/ffe-searchable-dropdown-react/src/types.ts b/packages/ffe-searchable-dropdown-react/src/types.ts new file mode 100644 index 0000000000..cc03eb0cc8 --- /dev/null +++ b/packages/ffe-searchable-dropdown-react/src/types.ts @@ -0,0 +1,22 @@ +export type Locale = 'nb' | 'nn' | 'en'; + +export type StateChange = + | 'InputFocus' + | 'InputBlur' + | 'InputClick' + | 'InputChange' + | 'InputKeyDownEscape' + | 'InputKeyDownEnter' + | 'InputKeyDownArrowDown' + | 'InputKeyDownArrowUp' + | 'ToggleButtonPressed' + | 'ItemOnMouseDown' + | 'FocusMovedOutSide' + | 'ItemSelectedProgrammatically' + | 'DropdownListPropUpdated' + | 'ItemOnClick'; + +export type SearchMatcher> = ( + inputValue: string, + searchAttributes: Array, +) => (item: Item) => boolean; diff --git a/packages/ffe-searchable-dropdown-react/tsconfig.cjs.json b/packages/ffe-searchable-dropdown-react/tsconfig.cjs.json new file mode 100644 index 0000000000..6579fd2246 --- /dev/null +++ b/packages/ffe-searchable-dropdown-react/tsconfig.cjs.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./lib", + "module": "commonjs" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "src/**/*.spec.ts*"] +} diff --git a/packages/ffe-searchable-dropdown-react/tsconfig.esm.json b/packages/ffe-searchable-dropdown-react/tsconfig.esm.json new file mode 100644 index 0000000000..8e577796bf --- /dev/null +++ b/packages/ffe-searchable-dropdown-react/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./es", + "module": "esnext" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "src/**/*.spec.ts*"] +} diff --git a/packages/ffe-searchable-dropdown-react/tsconfig.types.json b/packages/ffe-searchable-dropdown-react/tsconfig.types.json new file mode 100644 index 0000000000..3499c0be03 --- /dev/null +++ b/packages/ffe-searchable-dropdown-react/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./types", + "declaration": true, + "emitDeclarationOnly": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "src/**/*.spec.ts*"] +} diff --git a/packages/ffe-system-message-react/README.md b/packages/ffe-system-message-react/README.md index 7a16903e65..65732d4166 100644 --- a/packages/ffe-system-message-react/README.md +++ b/packages/ffe-system-message-react/README.md @@ -10,11 +10,6 @@ npm install --save @sb1/ffe-system-message-react Full documentation on system message usage is available at https://design.sparebank1.no/komponenter/meldinger/#systemmessage. -## TypeScript definition files - -This component supports TypeScript - please update `index.d.ts` if you change any -of the external methods or properties in this component. - ## Development To start a local development server, run the following from the designsystem root folder: diff --git a/packages/ffe-system-message-react/package.json b/packages/ffe-system-message-react/package.json index f838492931..6c1d957b67 100644 --- a/packages/ffe-system-message-react/package.json +++ b/packages/ffe-system-message-react/package.json @@ -17,19 +17,19 @@ "url": "ssh://git@github.com:SpareBank1/designsystem.git" }, "scripts": { - "build": "ffe-buildtool babel", + "build": "ffe-buildtool tsc", "watch": "ffe-buildtool babel-watch", - "lint": "eslint src", - "lint:fix": "eslint src --fix", + "lint": "eslint src --ext ts,tsx", + "lint:fix": "eslint src --fix --ext ts,tsx", "test": "ffe-buildtool jest", "test:watch": "ffe-buildtool jest --watch" }, "dependencies": { "@sb1/ffe-grid-react": "^13.2.15", + "@sb1/ffe-collapse-react": "^4.0.2", "@sb1/ffe-icons-react": "^10.0.1", "@sb1/ffe-system-message": "^8.0.17", - "classnames": "^2.3.1", - "prop-types": "^15.7.2" + "classnames": "^2.3.1" }, "devDependencies": { "@sb1/ffe-buildtool": "^0.6.1", diff --git a/packages/ffe-system-message-react/src/SystemErrorMessage.js b/packages/ffe-system-message-react/src/SystemErrorMessage.tsx similarity index 56% rename from packages/ffe-system-message-react/src/SystemErrorMessage.js rename to packages/ffe-system-message-react/src/SystemErrorMessage.tsx index ec3cb0c07f..fa8b0fcb1b 100644 --- a/packages/ffe-system-message-react/src/SystemErrorMessage.js +++ b/packages/ffe-system-message-react/src/SystemErrorMessage.tsx @@ -1,19 +1,19 @@ import React from 'react'; -import { func, string, number, node, oneOf, bool } from 'prop-types'; import { Icon } from '@sb1/ffe-icons-react'; -import texts from './texts'; +import { texts } from './texts'; +import { SystemMessage, SystemMessageProps } from './SystemMessage'; -import SystemMessage from './SystemMessage'; - -export default function SystemErrorMessage(props) { - const { - alert = true, - locale = 'nb', - animationLengthMs = 300, - onClose = f => f, - ...rest - } = props; +export interface SystemErrorMessageProps + extends Omit { + /** When false, role is not set to alert, avoids message from being read up immediately after page load. Default value is true. */ + alert?: boolean; +} +export const SystemErrorMessage: React.FC = ({ + locale = 'nb', + alert, + ...rest +}) => { const priorityHighIconSmall = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIiB3aWR0aD0iMjAiPjxwYXRoIGQ9Ik00NzkuNzg4LTE4Ny4wOHEtMjEuNTM3IDAtMzYuNjYyLTE1LjMzN3QtMTUuMTI1LTM2Ljg3NHEwLTIxLjUzNyAxNS4zMzctMzYuNjYyIDE1LjMzNy0xNS4xMjQgMzYuODc0LTE1LjEyNCAyMS41MzcgMCAzNi42NjIgMTUuMzM3dDE1LjEyNSAzNi44NzRxMCAyMS41MzctMTUuMzM3IDM2LjY2MS0xNS4zMzcgMTUuMTI1LTM2Ljg3NCAxNS4xMjVabS0uMDYyLTE5Ni4xNTFxLTE5LjM0MSAwLTMyLjg0LTEzLjcwOS0xMy41LTEzLjcwOS0xMy41LTMyLjk2di0zMTYuNjg1cTAtMTkuMjUxIDEzLjc3NC0zMi43OTQgMTMuNzczLTEzLjU0MiAzMy4xMTQtMTMuNTQyIDE5LjM0MSAwIDMyLjg0IDEzLjcwOSAxMy41IDEzLjcwOSAxMy41IDMyLjk2djMxNi42ODVxMCAxOS4yNTEtMTMuNzc0IDMyLjc5NC0xMy43NzMgMTMuNTQyLTMzLjExNCAxMy41NDJaIi8+PC9zdmc+'; return ( @@ -23,28 +23,7 @@ export default function SystemErrorMessage(props) { locale={locale} icon={} role={alert ? 'alert' : 'group'} - animationLengthMs={animationLengthMs} - onClose={onClose} {...rest} /> ); -} - -SystemErrorMessage.propTypes = { - animationLengthMs: number, - /** The content of the system message */ - children: node.isRequired, - /** Additional classes added to the surrounding div */ - className: string, - /** 'nb', 'nn', or 'en' */ - locale: oneOf(['en', 'nb', 'nn']), - /** - * The type of system message. Used internally only- - * @ignore - **/ - modifier: oneOf(['error', 'info', 'success', 'news']), - /** Callback for when the system message has been closed (after animation ends) */ - onClose: func, - /** When false, role is not set to alert, avoids message from being read up immediately after page load. Default value is true. */ - alert: bool, }; diff --git a/packages/ffe-system-message-react/src/SystemInfoMessage.js b/packages/ffe-system-message-react/src/SystemInfoMessage.tsx similarity index 77% rename from packages/ffe-system-message-react/src/SystemInfoMessage.js rename to packages/ffe-system-message-react/src/SystemInfoMessage.tsx index 104af5cb24..9ca809d938 100644 --- a/packages/ffe-system-message-react/src/SystemInfoMessage.js +++ b/packages/ffe-system-message-react/src/SystemInfoMessage.tsx @@ -1,10 +1,14 @@ import React from 'react'; import { Icon } from '@sb1/ffe-icons-react'; -import texts from './texts'; -import SystemMessage from './SystemMessage'; -import { oneOf } from 'prop-types'; +import { texts } from './texts'; +import { SystemMessage, SystemMessageProps } from './SystemMessage'; +export interface SystemInfoMessageProps + extends Omit {} -export default function SystemInfoMessage({ locale = 'nb', ...rest }) { +export const SystemInfoMessage: React.FC = ({ + locale = 'nb', + ...rest +}) => { const infoIconSmall = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIiB3aWR0aD0iMjAiPjxwYXRoIGQ9Ik00NzkuNzg4LTY3MnEtMjUuOTQyIDAtNDMuODY0LTE4LjEzNS0xNy45MjMtMTguMTM2LTE3LjkyMy00NC4wNzd0MTguMTM1LTQzLjg2NHExOC4xMzUtMTcuOTIzIDQ0LjA3Ni0xNy45MjMgMjUuOTQyIDAgNDMuODY0IDE4LjEzNiAxNy45MjMgMTguMTM1IDE3LjkyMyA0NC4wNzZ0LTE4LjEzNSA0My44NjRRNTA1LjcyOS02NzIgNDc5Ljc4OC02NzJabS4yNTcgNTA3Ljk5OXEtMjAuODE0IDAtMzUuNDI5LTE0LjU4NC0xNC42MTUtMTQuNTgzLTE0LjYxNS0zNS40MTZ2LTI5Ni42MTRxMC0yMC44MzMgMTQuNTctMzUuNDE2IDE0LjU3LTE0LjU4MyAzNS4zODQtMTQuNTgzdDM1LjQyOSAxNC41ODNxMTQuNjE1IDE0LjU4MyAxNC42MTUgMzUuNDE2djI5Ni42MTRxMCAyMC44MzMtMTQuNTcgMzUuNDE2LTE0LjU3IDE0LjU4NC0zNS4zODQgMTQuNTg0WiIvPjwvc3ZnPg=='; return ( @@ -16,9 +20,4 @@ export default function SystemInfoMessage({ locale = 'nb', ...rest }) { {...rest} /> ); -} - -SystemInfoMessage.propTypes = { - /** 'nb', 'nn', or 'en' */ - locale: oneOf(['en', 'nb', 'nn']), }; diff --git a/packages/ffe-system-message-react/src/SystemMessage.spec.js b/packages/ffe-system-message-react/src/SystemMessage.spec.js deleted file mode 100644 index d03e3ef6e9..0000000000 --- a/packages/ffe-system-message-react/src/SystemMessage.spec.js +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react'; - -import { - SystemErrorMessage, - SystemInfoMessage, - SystemNewsMessage, - SystemSuccessMessage, -} from '.'; - -const defaultProps = { - children: Message, -}; - -const getWrapper = Comp => props => - mount(); - -const getErrorWrapper = getWrapper(SystemErrorMessage); -const getInfoWrapper = getWrapper(SystemInfoMessage); -const getNewsWrapper = getWrapper(SystemNewsMessage); -const getSuccessWrapper = getWrapper(SystemSuccessMessage); - -describe('', () => { - it('renders without exploding', () => { - const wrapper = getInfoWrapper(); - expect(wrapper.exists()).toBe(true); - }); - it('applies the correct modifier classes to each type', () => { - const errorWrapper = getErrorWrapper(); - expect( - errorWrapper - .find('.ffe-system-message-wrapper') - .hasClass('ffe-system-message-wrapper--error'), - ).toBe(true); - - const infoWrapper = getInfoWrapper(); - expect( - infoWrapper - .find('.ffe-system-message-wrapper') - .hasClass('ffe-system-message-wrapper--info'), - ).toBe(true); - - const newsWrapper = getNewsWrapper(); - expect( - newsWrapper - .find('.ffe-system-message-wrapper') - .hasClass('ffe-system-message-wrapper--news'), - ).toBe(true); - - const successWrapper = getSuccessWrapper(); - expect( - successWrapper - .find('.ffe-system-message-wrapper') - .hasClass('ffe-system-message-wrapper--success'), - ).toBe(true); - }); - it('renders with correct aria-label', () => { - const infoWrapper = getInfoWrapper(); - const successWrapper = getSuccessWrapper(); - const infoEl = infoWrapper.find('.ffe-system-message-wrapper'); - const successEl = successWrapper.find('.ffe-system-message-wrapper'); - expect(infoEl.prop('aria-label')).toBe('Infomelding'); - expect(successEl.prop('aria-label')).toBe('Suksessmelding'); - }); - it('renders with role group on container', () => { - const infoWrapper = getInfoWrapper(); - const infoEl = infoWrapper.find('.ffe-system-message-wrapper'); - expect(infoEl.prop('role')).toBe('group'); - }); - - it('collapses when close button is clicked', done => { - const wrapper = getInfoWrapper({ - animationLengthMs: 10, - }); - expect(wrapper.find('.ffe-system-message-wrapper').exists()).toBe(true); - wrapper.find('button').simulate('click'); - - setTimeout(() => { - wrapper.update(); - expect(wrapper.find('.ffe-system-message-wrapper').exists()).toBe( - false, - ); - done(); - }, 20); - }); - it('calls onClose prop when close button is clicked', done => { - const onClickSpy = jest.fn(); - const wrapper = getInfoWrapper({ - animationLengthMs: 10, - onClose: onClickSpy, - }); - expect(wrapper.find('.ffe-system-message-wrapper').exists()).toBe(true); - wrapper.find('button').simulate('click'); - - setTimeout(() => { - wrapper.update(); - expect(onClickSpy).toHaveBeenCalledTimes(1); - done(); - }, 20); - }); - it('renders a Norwegian aria label on the close button by default', () => { - const wrapper = getInfoWrapper(); - expect(wrapper.find('button').prop('aria-label')).toBe('Lukk'); - }); - it('renders an English aria-label if locale is "en"', () => { - const wrapper = getInfoWrapper({ - locale: 'en', - }); - expect(wrapper.find('button').prop('aria-label')).toBe('Close'); - }); -}); diff --git a/packages/ffe-system-message-react/src/SystemMessage.spec.tsx b/packages/ffe-system-message-react/src/SystemMessage.spec.tsx new file mode 100644 index 0000000000..117dacf4b6 --- /dev/null +++ b/packages/ffe-system-message-react/src/SystemMessage.spec.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { + SystemErrorMessage, + SystemErrorMessageProps, +} from './SystemErrorMessage'; +import { SystemInfoMessage, SystemInfoMessageProps } from './SystemInfoMessage'; +import { SystemNewsMessage, SystemNewsMessageProps } from './SystemNewsMessage'; +import { + SystemSuccessMessage, + SystemSuccessMessageProps, +} from './SystemSuccessMessage'; +import { render, screen } from '@testing-library/react'; + +const defaultProps = { + children: Message, +}; + +const TEST_ID_ERROR = 'TEST_ID_ERROR'; +const TEST_ID_INFO = 'TEST_ID_INFO'; +const TEST_ID_NEWS = 'TEST_ID_NEWS'; +const TEST_ID_SUCCESS = 'TEST_ID_SUCCESS'; + +const renderSystemErrorMessage = (props?: SystemErrorMessageProps) => + render( + , + ); +const renderSystemInfoMessage = (props?: SystemInfoMessageProps) => + render( + , + ); +const renderSystemNewsMessage = (props?: SystemNewsMessageProps) => + render( + , + ); +const renderSystemSuccessMessage = (props?: SystemSuccessMessageProps) => + render( + , + ); + +describe('', () => { + it('applies the correct modifier classes to each type', () => { + renderSystemErrorMessage(); + const error = screen.getByTestId(TEST_ID_ERROR); + expect(error.classList.contains('ffe-system-message-wrapper')).toBe( + true, + ); + expect( + error.classList.contains('ffe-system-message-wrapper--error'), + ).toBe(true); + + renderSystemInfoMessage(); + const info = screen.getByTestId(TEST_ID_INFO); + expect(info.classList.contains('ffe-system-message-wrapper')).toBe( + true, + ); + expect( + info.classList.contains('ffe-system-message-wrapper--info'), + ).toBe(true); + + renderSystemNewsMessage(); + const news = screen.getByTestId(TEST_ID_NEWS); + expect(news.classList.contains('ffe-system-message-wrapper')).toBe( + true, + ); + expect( + news.classList.contains('ffe-system-message-wrapper--news'), + ).toBe(true); + + renderSystemSuccessMessage(); + const success = screen.getByTestId(TEST_ID_SUCCESS); + expect(success.classList.contains('ffe-system-message-wrapper')).toBe( + true, + ); + expect( + success.classList.contains('ffe-system-message-wrapper--success'), + ).toBe(true); + }); + it('renders with correct aria-label', () => { + renderSystemInfoMessage(); + const info = screen.getByTestId(TEST_ID_INFO); + renderSystemSuccessMessage(); + const success = screen.getByTestId(TEST_ID_SUCCESS); + expect(info.getAttribute('aria-label')).toBe('Infomelding'); + expect(success.getAttribute('aria-label')).toBe('Suksessmelding'); + }); + it('renders with role group on container', () => { + renderSystemInfoMessage(); + const info = screen.getByTestId(TEST_ID_INFO); + expect(info.getAttribute('role')).toBe('group'); + }); + + it('renders a Norwegian aria label on the close button by default', () => { + renderSystemInfoMessage(); + expect( + screen.queryByRole('button', { name: 'Lukk' }), + ).toBeInTheDocument(); + }); + it('renders an English aria-label if locale is "en"', () => { + renderSystemInfoMessage({ locale: 'en' }); + expect( + screen.queryByRole('button', { name: 'Close' }), + ).toBeInTheDocument(); + }); +}); diff --git a/packages/ffe-system-message-react/src/SystemMessage.js b/packages/ffe-system-message-react/src/SystemMessage.tsx similarity index 52% rename from packages/ffe-system-message-react/src/SystemMessage.js rename to packages/ffe-system-message-react/src/SystemMessage.tsx index c49c7cb189..4827496376 100644 --- a/packages/ffe-system-message-react/src/SystemMessage.js +++ b/packages/ffe-system-message-react/src/SystemMessage.tsx @@ -1,50 +1,39 @@ -import React, { Component } from 'react'; -import { func, string, number, node, oneOf, bool } from 'prop-types'; +import React, { useState } from 'react'; import classNames from 'classnames'; import { Icon } from '@sb1/ffe-icons-react'; -import texts from './texts'; +import { texts } from './texts'; +import { Collapse } from '@sb1/ffe-collapse-react'; -class SystemMessage extends Component { - constructor() { - super(); - this.close = this.close.bind(this); - this.state = { - closed: false, - }; - } - - close(event) { - const { animationLengthMs, onClose = f => f } = this.props; - const self = this._self; - self.style.height = `${self.offsetHeight}px`; - setTimeout(() => { - self.style.height = 0; - }, 0); - setTimeout(() => { - this.setState({ closed: true }, () => { - onClose(event); - }); - }, animationLengthMs); - return false; - } - - render() { - const { - animationLengthMs = 300, - children, - className, - icon, - locale = 'nb', - modifier, - onColoredBg, - ...rest - } = this.props; +export interface SystemMessageProps + extends React.ComponentPropsWithoutRef<'div'> { + icon?: React.ReactNode; + locale?: 'en' | 'nb' | 'nn'; + /** Callback for when the system message has been closed (after animation ends) */ + onCloseRest?: () => void; + /** Adds alternative styling for better contrast on certain backgrounds */ + onColoredBg?: boolean; + /** + * The type of system message. Used internally only- + * @ignore + **/ + modifier: 'error' | 'info' | 'success' | 'news'; +} - if (this.state.closed) { - return null; - } +export const SystemMessage: React.FC = ({ + children, + className, + icon, + locale = 'nb', + modifier, + onColoredBg, + onClick, + onCloseRest, + ...rest +}) => { + const [isOpen, setIsOpen] = useState(true); - return ( + return ( +
{ - this._self = _self; - }} - style={{ - transition: `height ${animationLengthMs / 1000}s`, - }} {...rest} >
- {icon && + {React.isValidElement(icon) && React.cloneElement(icon, { ...icon.props, })} @@ -72,7 +55,7 @@ class SystemMessage extends Component {
- ); - } -} - -SystemMessage.propTypes = { - animationLengthMs: number, - /** The content of the system message */ - children: node.isRequired, - /** Additional classes added to the surrounding div */ - className: string, - /** Override the default icon - use with caution! */ - icon: node.isRequired, - /** 'nb', 'nn', or 'en' */ - locale: oneOf(['en', 'nb', 'nn']), - /** - * The type of system message. Used internally only- - * @ignore - **/ - modifier: oneOf(['error', 'info', 'success', 'news']), - /** Callback for when the system message has been closed (after animation ends) */ - onClose: func, - /* Adds alternative styling for better contrast on certain backgrounds */ - onColoredBg: bool, +
+ ); }; - -export default SystemMessage; diff --git a/packages/ffe-system-message-react/src/SystemNewsMessage.js b/packages/ffe-system-message-react/src/SystemNewsMessage.tsx similarity index 77% rename from packages/ffe-system-message-react/src/SystemNewsMessage.js rename to packages/ffe-system-message-react/src/SystemNewsMessage.tsx index 1dffbccd93..0e215c3e51 100644 --- a/packages/ffe-system-message-react/src/SystemNewsMessage.js +++ b/packages/ffe-system-message-react/src/SystemNewsMessage.tsx @@ -1,10 +1,15 @@ import React from 'react'; import { Icon } from '@sb1/ffe-icons-react'; -import texts from './texts'; -import SystemMessage from './SystemMessage'; -import { oneOf } from 'prop-types'; +import { texts } from './texts'; +import { SystemMessage, SystemMessageProps } from './SystemMessage'; -export default function SystemNewsMessage({ locale = 'nb', ...rest }) { +export interface SystemNewsMessageProps + extends Omit {} + +export const SystemNewsMessage: React.FC = ({ + locale = 'nb', + ...rest +}) => { const infoIconSmall = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIiB3aWR0aD0iMjAiPjxwYXRoIGQ9Ik00NzkuNzg4LTY3MnEtMjUuOTQyIDAtNDMuODY0LTE4LjEzNS0xNy45MjMtMTguMTM2LTE3LjkyMy00NC4wNzd0MTguMTM1LTQzLjg2NHExOC4xMzUtMTcuOTIzIDQ0LjA3Ni0xNy45MjMgMjUuOTQyIDAgNDMuODY0IDE4LjEzNiAxNy45MjMgMTguMTM1IDE3LjkyMyA0NC4wNzZ0LTE4LjEzNSA0My44NjRRNTA1LjcyOS02NzIgNDc5Ljc4OC02NzJabS4yNTcgNTA3Ljk5OXEtMjAuODE0IDAtMzUuNDI5LTE0LjU4NC0xNC42MTUtMTQuNTgzLTE0LjYxNS0zNS40MTZ2LTI5Ni42MTRxMC0yMC44MzMgMTQuNTctMzUuNDE2IDE0LjU3LTE0LjU4MyAzNS4zODQtMTQuNTgzdDM1LjQyOSAxNC41ODNxMTQuNjE1IDE0LjU4MyAxNC42MTUgMzUuNDE2djI5Ni42MTRxMCAyMC44MzMtMTQuNTcgMzUuNDE2LTE0LjU3IDE0LjU4NC0zNS4zODQgMTQuNTg0WiIvPjwvc3ZnPg=='; return ( @@ -16,9 +21,4 @@ export default function SystemNewsMessage({ locale = 'nb', ...rest }) { {...rest} /> ); -} - -SystemNewsMessage.propTypes = { - /** 'nb', 'nn', or 'en' */ - locale: oneOf(['en', 'nb', 'nn']), }; diff --git a/packages/ffe-system-message-react/src/SystemSuccessMessage.js b/packages/ffe-system-message-react/src/SystemSuccessMessage.tsx similarity index 74% rename from packages/ffe-system-message-react/src/SystemSuccessMessage.js rename to packages/ffe-system-message-react/src/SystemSuccessMessage.tsx index d4eb4668e1..af936e464a 100644 --- a/packages/ffe-system-message-react/src/SystemSuccessMessage.js +++ b/packages/ffe-system-message-react/src/SystemSuccessMessage.tsx @@ -1,10 +1,15 @@ import React from 'react'; import { Icon } from '@sb1/ffe-icons-react'; -import texts from './texts'; -import SystemMessage from './SystemMessage'; -import { oneOf } from 'prop-types'; +import { texts } from './texts'; +import { SystemMessage, SystemMessageProps } from './SystemMessage'; -export default function SystemSuccessMessage({ locale = 'nb', ...rest }) { +export interface SystemSuccessMessageProps + extends Omit {} + +export const SystemSuccessMessage: React.FC = ({ + locale = 'nb', + ...rest +}) => { const checkIconSmall = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIiB3aWR0aD0iMjAiPjxwYXRoIGQ9Im0zOTUtMzcyLjM4NCAyNzAuNTM5LTI2OS41MzhxNy45MjMtNy45MjMgMTcuNjkyLTcuNjE2IDkuNzY5LjMwOCAxOC4wNzYgOC42MTYgOC4zMDggOC4zMDcgOC4zMDggMTcuODg0IDAgOS41NzYtOC4zMDggMTcuODg0bC0yODMgMjgyLjk5OXEtOS44NDYgOS44NDYtMjIuODA3IDkuODQ2LTEyLjk2MSAwLTIyLjgwNy05Ljg0NmwtMTE0LTExMy45OTlxLTcuOTIzLTcuOTIzLTguMzA4LTE3LjY5Mi0uMzg0LTkuNzY5IDcuOTIzLTE4LjA3NiA4LjMwOC04LjMwOCAxNy44ODQtOC4zMDggOS41NzcgMCAxNy44ODQgOC4zMDhMMzk1LTM3Mi4zODRaIi8+PC9zdmc+'; return ( @@ -16,9 +21,4 @@ export default function SystemSuccessMessage({ locale = 'nb', ...rest }) { {...rest} /> ); -} - -SystemSuccessMessage.propTypes = { - /** 'nb', 'nn', or 'en' */ - locale: oneOf(['en', 'nb', 'nn']), }; diff --git a/packages/ffe-system-message-react/src/index.d.ts b/packages/ffe-system-message-react/src/index.d.ts deleted file mode 100644 index 060193304d..0000000000 --- a/packages/ffe-system-message-react/src/index.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; - -export interface SystemMessageProps - extends React.ComponentPropsWithoutRef<'div'> { - animationLengthMs?: number; - icon?: React.ReactNode; - locale?: 'en' | 'nb' | 'nn'; - onClose?: React.MouseEventHandler; - onColoredBg?: boolean; -} - -export interface SystemErrorMessageProps extends SystemMessageProps { - alert?: boolean; -} - -declare class SystemErrorMessage extends React.Component< - SystemErrorMessageProps, - any -> {} -declare class SystemInfoMessage extends React.Component< - SystemMessageProps, - any -> {} -declare class SystemNewsMessage extends React.Component< - SystemMessageProps, - any -> {} -declare class SystemSuccessMessage extends React.Component< - SystemMessageProps, - any -> {} diff --git a/packages/ffe-system-message-react/src/index.js b/packages/ffe-system-message-react/src/index.js deleted file mode 100644 index 52c574db40..0000000000 --- a/packages/ffe-system-message-react/src/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import SystemErrorMessage from './SystemErrorMessage'; -import SystemInfoMessage from './SystemInfoMessage'; -import SystemNewsMessage from './SystemNewsMessage'; -import SystemSuccessMessage from './SystemSuccessMessage'; - -export { - SystemErrorMessage, - SystemInfoMessage, - SystemNewsMessage, - SystemSuccessMessage, -}; diff --git a/packages/ffe-system-message-react/src/index.ts b/packages/ffe-system-message-react/src/index.ts new file mode 100644 index 0000000000..cd5b087887 --- /dev/null +++ b/packages/ffe-system-message-react/src/index.ts @@ -0,0 +1,10 @@ +export { + SystemErrorMessage, + SystemErrorMessageProps, +} from './SystemErrorMessage'; +export { SystemInfoMessage, SystemInfoMessageProps } from './SystemInfoMessage'; +export { SystemNewsMessage, SystemNewsMessageProps } from './SystemNewsMessage'; +export { + SystemSuccessMessage, + SystemSuccessMessageProps, +} from './SystemSuccessMessage'; diff --git a/packages/ffe-system-message-react/src/texts.js b/packages/ffe-system-message-react/src/texts.ts similarity index 91% rename from packages/ffe-system-message-react/src/texts.js rename to packages/ffe-system-message-react/src/texts.ts index 22889b83a0..f9f6212484 100644 --- a/packages/ffe-system-message-react/src/texts.js +++ b/packages/ffe-system-message-react/src/texts.ts @@ -12,7 +12,7 @@ const nb = { news: { ariaLabel: 'Nyhetsmelding', }, -}; +} as const; const nn = { close: 'Lukk', error: { @@ -27,7 +27,7 @@ const nn = { news: { ariaLabel: 'Nyheitsmelding', }, -}; +} as const; const en = { close: 'Close', error: { @@ -42,6 +42,6 @@ const en = { news: { ariaLabel: 'News message', }, -}; +} as const; -export default { nb, nn, en }; +export const texts = { nb, nn, en }; diff --git a/packages/ffe-system-message-react/tsconfig.cjs.json b/packages/ffe-system-message-react/tsconfig.cjs.json new file mode 100644 index 0000000000..38b3f1a47e --- /dev/null +++ b/packages/ffe-system-message-react/tsconfig.cjs.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./lib", + "module": "commonjs" + }, + "include": ["src/**/*"], + "exclude": ["../ffe-system-message/node_modules", "src/**/*.spec.ts*"] +} diff --git a/packages/ffe-system-message-react/tsconfig.esm.json b/packages/ffe-system-message-react/tsconfig.esm.json new file mode 100644 index 0000000000..94d4abc8be --- /dev/null +++ b/packages/ffe-system-message-react/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./es", + "module": "esnext" + }, + "include": ["src/**/*"], + "exclude": ["../ffe-system-message/node_modules", "src/**/*.spec.ts*"] +} diff --git a/packages/ffe-system-message-react/tsconfig.types.json b/packages/ffe-system-message-react/tsconfig.types.json new file mode 100644 index 0000000000..63401b661c --- /dev/null +++ b/packages/ffe-system-message-react/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./types", + "declaration": true, + "emitDeclarationOnly": true + }, + "include": ["src/**/*"], + "exclude": ["../ffe-system-message/node_modules", "src/**/*.spec.ts*"] +}