@ndla/ui 27.1.7 → 29.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/es/Frontpage/FrontpageAllSubjects.js +9 -8
  2. package/es/Resource/BlockResource.js +22 -13
  3. package/es/Resource/ListResource.js +24 -15
  4. package/es/Resource/resourceComponents.js +28 -28
  5. package/es/TagSelector/Control.js +23 -0
  6. package/es/TagSelector/DropdownIndicator.js +66 -0
  7. package/es/TagSelector/Input.js +19 -0
  8. package/es/TagSelector/Menu.js +26 -0
  9. package/es/TagSelector/MenuList.js +22 -0
  10. package/es/TagSelector/Option.js +55 -0
  11. package/es/TagSelector/SelectContainer.js +18 -0
  12. package/es/TagSelector/TagSelector.js +161 -100
  13. package/es/TagSelector/ValueButton.js +46 -0
  14. package/es/TagSelector/ariaMessages.js +104 -0
  15. package/es/TagSelector/index.js +2 -1
  16. package/es/TagSelector/types.js +0 -0
  17. package/es/TreeStructure/ComboboxButton.js +19 -18
  18. package/es/TreeStructure/TreeStructure.js +8 -8
  19. package/es/locale/messages-en.js +46 -8
  20. package/es/locale/messages-nb.js +57 -19
  21. package/es/locale/messages-nn.js +56 -18
  22. package/es/locale/messages-se.js +48 -10
  23. package/es/locale/messages-sma.js +57 -19
  24. package/es/model/ContentType.js +23 -1
  25. package/es/model/SubjectCategories.js +1 -5
  26. package/es/model/index.js +3 -2
  27. package/lib/Frontpage/FrontpageAllSubjects.js +10 -8
  28. package/lib/Resource/BlockResource.d.ts +6 -3
  29. package/lib/Resource/BlockResource.js +23 -12
  30. package/lib/Resource/ListResource.d.ts +6 -3
  31. package/lib/Resource/ListResource.js +25 -14
  32. package/lib/Resource/resourceComponents.d.ts +6 -3
  33. package/lib/Resource/resourceComponents.js +30 -30
  34. package/lib/TagSelector/Control.d.ts +12 -0
  35. package/lib/TagSelector/Control.js +35 -0
  36. package/lib/TagSelector/DropdownIndicator.d.ts +12 -0
  37. package/lib/TagSelector/DropdownIndicator.js +80 -0
  38. package/lib/TagSelector/Input.d.ts +12 -0
  39. package/lib/TagSelector/Input.js +33 -0
  40. package/lib/TagSelector/Menu.d.ts +12 -0
  41. package/lib/TagSelector/Menu.js +40 -0
  42. package/lib/TagSelector/MenuList.d.ts +13 -0
  43. package/lib/TagSelector/MenuList.js +36 -0
  44. package/lib/TagSelector/Option.d.ts +12 -0
  45. package/lib/TagSelector/Option.js +61 -0
  46. package/lib/TagSelector/SelectContainer.d.ts +12 -0
  47. package/lib/TagSelector/SelectContainer.js +31 -0
  48. package/lib/TagSelector/TagSelector.d.ts +14 -11
  49. package/lib/TagSelector/TagSelector.js +165 -96
  50. package/lib/TagSelector/ValueButton.d.ts +16 -0
  51. package/lib/TagSelector/ValueButton.js +55 -0
  52. package/lib/TagSelector/ariaMessages.d.ts +16 -0
  53. package/lib/TagSelector/ariaMessages.js +113 -0
  54. package/lib/TagSelector/index.d.ts +2 -1
  55. package/lib/TagSelector/index.js +3 -5
  56. package/lib/TagSelector/types.d.ts +11 -0
  57. package/lib/TagSelector/types.js +1 -0
  58. package/lib/TreeStructure/ComboboxButton.js +19 -18
  59. package/lib/TreeStructure/TreeStructure.js +7 -7
  60. package/lib/locale/messages-en.d.ts +56 -25
  61. package/lib/locale/messages-en.js +46 -8
  62. package/lib/locale/messages-nb.d.ts +56 -25
  63. package/lib/locale/messages-nb.js +57 -19
  64. package/lib/locale/messages-nn.d.ts +56 -25
  65. package/lib/locale/messages-nn.js +56 -18
  66. package/lib/locale/messages-se.d.ts +56 -25
  67. package/lib/locale/messages-se.js +48 -10
  68. package/lib/locale/messages-sma.d.ts +56 -25
  69. package/lib/locale/messages-sma.js +57 -19
  70. package/lib/model/ContentType.d.ts +18 -0
  71. package/lib/model/ContentType.js +32 -2
  72. package/lib/model/SubjectCategories.d.ts +0 -3
  73. package/lib/model/SubjectCategories.js +3 -10
  74. package/lib/model/index.d.ts +12 -2
  75. package/lib/model/index.js +4 -3
  76. package/package.json +15 -14
  77. package/src/Frontpage/FrontpageAllSubjects.tsx +5 -2
  78. package/src/Resource/BlockResource.tsx +18 -11
  79. package/src/Resource/ListResource.tsx +14 -11
  80. package/src/Resource/resourceComponents.tsx +13 -14
  81. package/src/TagSelector/Control.tsx +34 -0
  82. package/src/TagSelector/DropdownIndicator.tsx +47 -0
  83. package/src/TagSelector/Input.tsx +31 -0
  84. package/src/TagSelector/Menu.tsx +38 -0
  85. package/src/TagSelector/MenuList.tsx +30 -0
  86. package/src/TagSelector/Option.tsx +53 -0
  87. package/src/TagSelector/SelectContainer.tsx +32 -0
  88. package/src/TagSelector/TagSelector.tsx +105 -84
  89. package/src/TagSelector/ValueButton.tsx +46 -0
  90. package/src/TagSelector/ariaMessages.ts +87 -0
  91. package/src/TagSelector/index.ts +2 -1
  92. package/src/TagSelector/types.ts +12 -0
  93. package/src/TreeStructure/ComboboxButton.tsx +15 -17
  94. package/src/TreeStructure/TreeStructure.tsx +2 -11
  95. package/src/locale/messages-en.ts +46 -9
  96. package/src/locale/messages-nb.ts +57 -20
  97. package/src/locale/messages-nn.ts +56 -19
  98. package/src/locale/messages-se.ts +48 -11
  99. package/src/locale/messages-sma.ts +57 -20
  100. package/src/model/ContentType.ts +29 -0
  101. package/src/model/SubjectCategories.ts +0 -5
  102. package/src/model/index.ts +2 -1
  103. package/es/TagSelector/SuggestionInput.js +0 -285
  104. package/es/TagSelector/Suggestions.js +0 -97
  105. package/lib/TagSelector/SuggestionInput.d.ts +0 -19
  106. package/lib/TagSelector/SuggestionInput.js +0 -299
  107. package/lib/TagSelector/Suggestions.d.ts +0 -12
  108. package/lib/TagSelector/Suggestions.js +0 -99
  109. package/src/.DS_Store +0 -0
  110. package/src/TagSelector/SuggestionInput.tsx +0 -287
  111. package/src/TagSelector/Suggestions.tsx +0 -139
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copyright (c) 2022-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import styled from '@emotion/styled';
10
+ import { colors, fonts, misc } from '@ndla/core';
11
+ import React from 'react';
12
+ import { ContainerProps } from 'react-select';
13
+ import { TagType } from './types';
14
+
15
+ const StyledContainer = styled.div`
16
+ display: grid;
17
+ grid-template-rows: auto 1fr;
18
+ overflow: hidden;
19
+
20
+ border: 1px solid ${colors.brand.neutral7};
21
+ border-radius: ${misc.borderRadius};
22
+ &:focus-within {
23
+ border-color: ${colors.brand.tertiary};
24
+ }
25
+ ${fonts.sizes(16)};
26
+ `;
27
+
28
+ const SelectContainer = ({ innerProps, selectProps, children }: ContainerProps<TagType, true>) => {
29
+ return <StyledContainer {...innerProps}>{children}</StyledContainer>;
30
+ };
31
+
32
+ export default SelectContainer;
@@ -1,4 +1,4 @@
1
- /*
1
+ /**
2
2
  * Copyright (c) 2022-present, NDLA.
3
3
  *
4
4
  * This source code is licensed under the GPLv3 license found in the
@@ -6,107 +6,128 @@
6
6
  *
7
7
  */
8
8
 
9
- import React, { useState, useRef, useEffect, ChangeEvent } from 'react';
9
+ import React, { KeyboardEvent, useMemo, useState } from 'react';
10
+ import CreatableSelect from 'react-select/creatable';
11
+ import { MultiValue, StylesConfig } from 'react-select';
10
12
  import styled from '@emotion/styled';
11
- import { spacingUnit, fonts } from '@ndla/core';
12
- import { uuid } from '@ndla/util';
13
- import SuggestionInput from './SuggestionInput';
13
+ import { colors, fonts, spacing, utils } from '@ndla/core';
14
+ import { useTranslation } from 'react-i18next';
15
+ import { TagType } from './types';
16
+ import ValueButton from './ValueButton';
17
+ import DropdownIndicator from './DropdownIndicator';
18
+ import SelectContainer from './SelectContainer';
19
+ import MenuList from './MenuList';
20
+ import Control from './Control';
21
+ import Option from './Option';
22
+ import Menu from './Menu';
23
+ import { createAriaMessages } from './ariaMessages';
24
+ import Input from './Input';
14
25
 
15
- const DEFAULT_DROPDOWN_MAXHEIGHT = '240px';
26
+ const styles: StylesConfig<TagType, true> = {
27
+ menu: () => ({}),
28
+ dropdownIndicator: () => ({}),
29
+ placeholder: (provided) => ({
30
+ ...provided,
31
+ padding: `0 ${spacing.small}`,
32
+ color: colors.brand.primary,
33
+ margin: 0,
34
+ }),
35
+ valueContainer: (provided) => ({ ...provided, padding: 0 }),
36
+ indicatorSeparator: () => ({
37
+ display: 'none',
38
+ }),
39
+ indicatorsContainer: (provided) => ({
40
+ ...provided,
41
+ alignSelf: 'flex-end',
42
+ }),
43
+ };
16
44
 
17
- const StyledLabel = styled.label`
18
- font-weight: ${fonts.weight.semibold};
45
+ const StyledTagSelector = styled.div`
46
+ display: flex;
47
+ flex-direction: column;
48
+ flex: 1;
49
+ overflow: hidden;
19
50
  `;
20
51
 
21
- export interface TagType {
22
- name: string;
23
- id: string;
52
+ interface StyledLabelProps {
53
+ labelHidden?: boolean;
24
54
  }
25
55
 
56
+ const StyledLabel = styled.label<StyledLabelProps>`
57
+ font-weight: ${fonts.weight.semibold};
58
+ ${(p) => p.labelHidden && utils.labelHidden}
59
+ `;
60
+
26
61
  interface Props {
27
62
  label: string;
28
- tags: TagType[];
29
- tagsSelected: string[];
30
- onToggleTag: (id: string) => void;
31
- onCreateTag: (tagName: string) => void;
32
- inline?: boolean;
33
- prefix?: string;
63
+ tags: string[];
64
+ selected: string[];
65
+ onChange: (tags: string[]) => void;
66
+ onCreateTag: (name: string) => void;
67
+ className?: string;
68
+ labelHidden?: boolean;
34
69
  }
35
70
 
36
- const sortedTags = (tags: TagType[], selectedTags: string[]): TagType[] => {
37
- const returnTags = selectedTags
38
- .map((selectedId) => tags.find(({ id }) => selectedId === id))
39
- .filter((notUndefined) => notUndefined) as unknown as TagType[];
40
- return returnTags;
41
- };
71
+ const TagSelector = ({ selected: _selected, tags: _tags, onChange, onCreateTag, className, label }: Props) => {
72
+ const { t } = useTranslation();
73
+ const [input, setInput] = useState('');
74
+ const tags = useMemo(() => _tags.map((tag) => ({ value: tag, label: tag })), [_tags]);
75
+ const selected = useMemo(() => _selected.map((tag) => ({ value: tag, label: tag })), [_selected]);
42
76
 
43
- const getSuggestions = (tags: TagType[], inputValue: string): TagType[] => {
44
- if (inputValue === '') {
45
- return [];
46
- }
47
- const inputLowercase = inputValue.toLowerCase();
48
- return tags
49
- .filter(({ name }) => name.toLowerCase().startsWith(inputLowercase))
50
- .sort((a, b) => a.name.localeCompare(b.name, 'nb'));
51
- };
52
-
53
- const TagSelector = ({ label, tags, tagsSelected, onCreateTag, onToggleTag, inline, prefix }: Props) => {
54
- const [inputValue, setInputValue] = useState('');
55
- const [expanded, setExpanded] = useState(false);
56
- const [dropdownMaxHeight, setDropdownMaxHeight] = useState(DEFAULT_DROPDOWN_MAXHEIGHT);
57
- const containerRef = useRef<HTMLDivElement>(null);
58
- const inputIdRef = useRef<string>(uuid());
59
-
60
- useEffect(() => {
61
- setExpanded(false);
62
- }, [tagsSelected]);
63
-
64
- useEffect(() => {
65
- const setMaxDropdownMaxHeight = () => {
66
- if (!inline && containerRef.current && typeof window !== 'undefined') {
67
- // Calculate distance from bottom of container to bottom of viewport
68
- const containerBottom = containerRef.current.getBoundingClientRect().bottom;
69
- const viewportBottom = document.documentElement.scrollHeight;
70
- const maxDropdownHeight = viewportBottom - containerBottom;
71
- setDropdownMaxHeight(`${maxDropdownHeight - spacingUnit}px`);
72
- }
73
- };
74
- if (!inline && typeof window !== 'undefined') {
75
- if (expanded) {
76
- setMaxDropdownMaxHeight();
77
- window.addEventListener('resize', setMaxDropdownMaxHeight);
78
- } else {
79
- window.removeEventListener('resize', setMaxDropdownMaxHeight);
77
+ const handleSpaceClick = (e: KeyboardEvent<HTMLDivElement>) => {
78
+ if (e.key === ' ') {
79
+ e.preventDefault();
80
+ if (!_selected.find((tag) => tag === input) && input !== '') {
81
+ onChange(_selected.concat(input));
80
82
  }
83
+ setInput('');
81
84
  }
82
- return () => {
83
- typeof window !== 'undefined' && window.removeEventListener('resize', setMaxDropdownMaxHeight);
84
- };
85
- }, [expanded, inline]);
85
+ };
86
+
87
+ const handleChange = (tags: MultiValue<TagType>) => {
88
+ onChange(tags.map((tag) => tag.value));
89
+ };
90
+
91
+ const createLabel = (tag: string) => t('tagSelector.createLabel', { tag });
86
92
 
87
93
  return (
88
- <div ref={containerRef}>
89
- <StyledLabel htmlFor={inputIdRef.current}>{label}</StyledLabel>
90
- <SuggestionInput
91
- onChange={(e: ChangeEvent<HTMLInputElement>) => {
92
- const target = e.target as HTMLInputElement;
93
- setInputValue(target.value);
94
- setExpanded(false);
94
+ <StyledTagSelector className={className}>
95
+ {label && (
96
+ <StyledLabel labelHidden htmlFor="tagselector-creatable" id="tagselector-label">
97
+ {label}
98
+ </StyledLabel>
99
+ )}
100
+ <CreatableSelect
101
+ id="tagselector-creatable"
102
+ aria-labelledby={label ? 'tagselector-label' : undefined}
103
+ ariaLiveMessages={createAriaMessages(t)}
104
+ components={{
105
+ DropdownIndicator,
106
+ MultiValue: ValueButton,
107
+ SelectContainer,
108
+ MenuList,
109
+ Control,
110
+ Option,
111
+ Menu,
112
+ Input,
95
113
  }}
96
- suggestions={expanded ? tags : getSuggestions(tags, inputValue)}
97
- value={inputValue}
98
- onCreateTag={onCreateTag}
99
- onToggleTag={onToggleTag}
100
- setInputValue={setInputValue}
101
- addedTags={sortedTags(tags, tagsSelected)}
102
- expanded={expanded}
103
- setExpanded={setExpanded}
104
- dropdownMaxHeight={dropdownMaxHeight}
105
- inline={inline}
106
- scrollAnchorElement={containerRef}
107
- prefix={prefix}
114
+ formatCreateLabel={createLabel}
115
+ inputValue={input}
116
+ isClearable={false}
117
+ isMulti
118
+ noOptionsMessage={() => t('tagSelector.noOptions')}
119
+ onChange={handleChange}
120
+ onCreateOption={onCreateTag}
121
+ onInputChange={setInput}
122
+ onKeyDown={handleSpaceClick}
123
+ options={tags}
124
+ placeholder={t('tagSelector.placeholder')}
125
+ screenReaderStatus={({ count }) => t('tagSelector.aria.screenReaderStatus', { count })}
126
+ styles={styles}
127
+ tabSelectsValue={false}
128
+ value={selected}
108
129
  />
109
- </div>
130
+ </StyledTagSelector>
110
131
  );
111
132
  };
112
133
 
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Copyright (c) 2022-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import React from 'react';
10
+ import { MultiValueProps } from 'react-select';
11
+ import { buttonStyleV2 as buttonStyle } from '@ndla/button';
12
+ import styled from '@emotion/styled';
13
+ import { colors, spacing } from '@ndla/core';
14
+ import { Cross } from '@ndla/icons/action';
15
+ import { TagType } from './types';
16
+
17
+ interface StyledProps {
18
+ selected: boolean;
19
+ }
20
+
21
+ export const StyledValueButton = styled.div<StyledProps>`
22
+ && {
23
+ background: ${({ selected }) => selected && colors.brand.primary};
24
+ color: ${({ selected }) => selected && colors.white};
25
+ padding: ${spacing.xxsmall} ${spacing.small};
26
+ margin: ${spacing.xxsmall};
27
+ border: none;
28
+ }
29
+ `;
30
+
31
+ const ValueButton = ({ innerProps, children, removeProps, isFocused }: MultiValueProps<TagType, true>) => {
32
+ return (
33
+ <StyledValueButton
34
+ selected={isFocused}
35
+ role="button"
36
+ css={buttonStyle({ colorTheme: 'lighter', shape: 'pill', size: 'small' })}
37
+ {...innerProps}
38
+ {...removeProps}>
39
+ <span aria-hidden>#</span>
40
+ {children}
41
+ <Cross />
42
+ </StyledValueButton>
43
+ );
44
+ };
45
+
46
+ export default ValueButton;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Copyright (c) 2022-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import { TFunction } from 'react-i18next';
10
+ import {
11
+ AriaGuidanceProps,
12
+ AriaOnChangeProps,
13
+ AriaOnFilterProps,
14
+ AriaOnFocusProps,
15
+ GroupBase,
16
+ OptionsOrGroups,
17
+ } from 'react-select';
18
+ import { TagType } from './types';
19
+
20
+ export const createAriaMessages = (t: TFunction) => ({
21
+ guidance: (props: AriaGuidanceProps) => {
22
+ const { isSearchable, isMulti, isDisabled, tabSelectsValue, context } = props;
23
+ switch (context) {
24
+ case 'menu':
25
+ return `${t('tagSelector.aria.guidance.menu.updown')}${
26
+ isDisabled ? '' : `, ${t('tagSelector.aria.guidance.menu.enter')}`
27
+ }, ${t('tagSelector.aria.guidance.menu.escape')}${
28
+ tabSelectsValue ? `, ${t('tagSelector.aria.guidance.menu.tab')}` : ''
29
+ }.`;
30
+ case 'input':
31
+ return `${props['aria-label'] || t('tagSelector.aria.guidance.input.select')} ${t(
32
+ 'tagSelector.aria.guidance.input.focused',
33
+ )} ${isSearchable ? `, ${t('tagSelector.aria.guidance.input.refine')}` : ''}, ${t(
34
+ 'tagSelector.aria.guidance.input.down',
35
+ )}, ${isMulti ? ` ${t('tagSelector.aria.guidance.input.left')}` : ''}, ${t(
36
+ 'tagSelector.aria.guidance.input.space',
37
+ )}`;
38
+ case 'value':
39
+ return t('tagSelector.aria.guidance.value');
40
+ default:
41
+ return '';
42
+ }
43
+ },
44
+
45
+ onChange: (props: AriaOnChangeProps<TagType, true>) => {
46
+ const { action, label = '', labels, isDisabled } = props;
47
+ switch (action) {
48
+ case 'deselect-option':
49
+ case 'pop-value':
50
+ case 'remove-value':
51
+ return t('tagSelector.aria.onChange.deselect', { label });
52
+ case 'clear':
53
+ return t('tagSelector.aria.onChange.clear');
54
+ case 'initial-input-focus':
55
+ return t('tagSelector.aria.onChange.initialFocus', { labels: labels.join(',') });
56
+ case 'select-option':
57
+ return isDisabled
58
+ ? t('tagSelector.aria.onChange.selectedDisabled', { label })
59
+ : t('tagSelector.aria.onChange.selected', { label });
60
+ default:
61
+ return '';
62
+ }
63
+ },
64
+
65
+ onFocus: (props: AriaOnFocusProps<TagType, GroupBase<TagType>>) => {
66
+ const { context, focused, options, label = '', selectValue, isDisabled, isSelected } = props;
67
+
68
+ const getArrayIndex = (arr: OptionsOrGroups<TagType, GroupBase<TagType>>, item: TagType) =>
69
+ arr && arr.length ? `${arr.indexOf(item) + 1} ${t('tagSelector.aria.onFocus.of')} ${arr.length}` : '';
70
+
71
+ if (context === 'value' && selectValue) {
72
+ return t('tagSelector.aria.onFocus.value', { label, position: getArrayIndex(selectValue, focused) });
73
+ }
74
+
75
+ if (context === 'menu') {
76
+ const disabled = isDisabled ? ` ${t('tagSelector.aria.disabled')}` : '';
77
+ const status = `${isSelected ? t('tagSelector.aria.selected') : t('tagSelector.aria.focused')}${disabled}`;
78
+ return t('tagSelector.aria.onFocus.menu', { label, status, position: getArrayIndex(options, focused) });
79
+ }
80
+ return '';
81
+ },
82
+
83
+ onFilter: (props: AriaOnFilterProps) => {
84
+ const { inputValue, resultsMessage } = props;
85
+ return `${resultsMessage}${inputValue ? ` ${t('tagSelector.aria.onFilter')} ` + inputValue : ''}.`;
86
+ },
87
+ });
@@ -6,7 +6,8 @@
6
6
  *
7
7
  */
8
8
 
9
- import TagSelector, { TagType } from './TagSelector';
9
+ import TagSelector from './TagSelector';
10
+ import { TagType } from './types';
10
11
 
11
12
  export type { TagType };
12
13
 
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Copyright (c) 2022-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ export interface TagType {
10
+ readonly value: string;
11
+ readonly label: string;
12
+ }
@@ -31,6 +31,7 @@ const StyledSelectedFolder = styled(Button)`
31
31
  flex: 1;
32
32
  justify-content: flex-start;
33
33
  color: ${colors.black};
34
+ border: none;
34
35
  :hover,
35
36
  :focus {
36
37
  background: none;
@@ -82,6 +83,7 @@ const ComboboxButton = forwardRef<HTMLButtonElement, Props>(
82
83
 
83
84
  const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
84
85
  if (e.key === 'Enter') {
86
+ onToggleTree(!showTree);
85
87
  if (showTree && focusedFolder) {
86
88
  setSelectedFolder(focusedFolder);
87
89
  }
@@ -89,6 +91,7 @@ const ComboboxButton = forwardRef<HTMLButtonElement, Props>(
89
91
  }
90
92
  if (e.key === 'Escape') {
91
93
  onToggleTree(false);
94
+ e.preventDefault();
92
95
  return;
93
96
  }
94
97
  if (['ArrowUp', 'ArrowDown'].includes(e.key) && !showTree) {
@@ -101,7 +104,16 @@ const ComboboxButton = forwardRef<HTMLButtonElement, Props>(
101
104
  };
102
105
 
103
106
  return (
104
- <StyledRow isOpen={showTree}>
107
+ <StyledRow
108
+ isOpen={showTree}
109
+ onMouseDown={(e) => {
110
+ if (!e.defaultPrevented) {
111
+ e.preventDefault();
112
+ e.stopPropagation();
113
+ onToggleTree(!showTree);
114
+ innerRef.current?.focus();
115
+ }
116
+ }}>
105
117
  <StyledSelectedFolder
106
118
  ref={innerRef}
107
119
  tabIndex={0}
@@ -116,24 +128,10 @@ const ComboboxButton = forwardRef<HTMLButtonElement, Props>(
116
128
  colorTheme="light"
117
129
  fontWeight="normal"
118
130
  shape="sharp"
119
- onKeyDown={onKeyDown}
120
- onClick={() => {
121
- innerRef.current?.focus();
122
- onToggleTree(!showTree);
123
- }}>
131
+ onKeyDown={onKeyDown}>
124
132
  {selectedFolder?.name}
125
133
  </StyledSelectedFolder>
126
- <IconButton
127
- aria-hidden
128
- aria-label=""
129
- tabIndex={-1}
130
- variant="ghost"
131
- colorTheme="greyLighter"
132
- size="small"
133
- onClick={() => {
134
- innerRef.current?.focus();
135
- onToggleTree(!showTree);
136
- }}>
134
+ <IconButton aria-hidden aria-label="" tabIndex={-1} variant="ghost" colorTheme="greyLighter" size="small">
137
135
  {showTree ? <ChevronUp /> : <ChevronDown />}
138
136
  </IconButton>
139
137
  </StyledRow>
@@ -8,7 +8,7 @@
8
8
 
9
9
  import React, { useEffect, useState, useMemo, useRef } from 'react';
10
10
  import styled from '@emotion/styled';
11
- import { colors, fonts, misc, spacing } from '@ndla/core';
11
+ import { colors, fonts, misc, utils } from '@ndla/core';
12
12
  import { css } from '@emotion/core';
13
13
  import { uniq } from 'lodash';
14
14
  import { IFolder } from '@ndla/types-learningpath-api';
@@ -62,16 +62,7 @@ const ScrollableDiv = styled.div<ScrollableDivProps>`
62
62
  type === 'picker' &&
63
63
  css`
64
64
  overflow: overlay;
65
- ::-webkit-scrollbar {
66
- width: ${spacing.small};
67
- }
68
- ::-webkit-scrollbar-thumb {
69
- border: 4px solid transparent;
70
- border-radius: 14px;
71
- background-clip: padding-box;
72
- padding: 0 4px;
73
- background-color: ${colors.brand.neutral7};
74
- }
65
+ ${utils.scrollbar}
75
66
  `}
76
67
  `;
77
68
 
@@ -30,11 +30,50 @@ const messages = {
30
30
  },
31
31
  },
32
32
  tagSelector: {
33
+ aria: {
34
+ screenReaderStatus: '{{count}} results available',
35
+ disabled: 'disabled',
36
+ selected: 'selected',
37
+ focused: 'focused',
38
+ guidance: {
39
+ menu: {
40
+ updown: 'Use Up and Down to choose tags',
41
+ enter: 'press Enter to select the currently focused tag',
42
+ escape: 'press Escape to exit the menu',
43
+ tab: 'press Tab to select the tag and exit the menu',
44
+ },
45
+ input: {
46
+ select: 'Tag menu',
47
+ focused: 'is focused',
48
+ refine: 'type to refine list',
49
+ down: 'press Down to open the menu',
50
+ left: 'press Left to focus selected tags',
51
+ space: 'press Space to create new tag',
52
+ },
53
+ value:
54
+ 'Use left and right to toggle between focused tags, press Backspace to remove the currently focused value. The last tag will be removed if none are selected.',
55
+ },
56
+ onChange: {
57
+ deselect: 'tag {{label}}, deselected.',
58
+ clear: 'All selected options have been cleared.',
59
+ initialFocus: `Tags {{labels}}, selected.`,
60
+ selectedDisabled: 'Tag {{label}} is disabled. Select another option.',
61
+ selected: 'Tag {{label}}, selected.',
62
+ },
63
+ onFocus: {
64
+ value: 'tag {{label}} focused, {{position}}.',
65
+ menu: 'tag {{label}} {{status}}, {{position}}.',
66
+ of: 'of',
67
+ },
68
+ onFilter: ' for search term ',
69
+ },
70
+ noOptions: 'No options',
33
71
  label: 'Add tag',
72
+ createLabel: 'Add tag {{tag}}',
34
73
  placeholder: 'Enter tag name',
35
74
  removeTag: 'Remove tag {{name}}',
36
- hideAllTags: 'Hide all tags',
37
- showAllTags: 'Show all tags',
75
+ hideTags: 'Hide tags',
76
+ showTags: 'Show tags',
38
77
  },
39
78
  htmlTitles: {
40
79
  titleTemplate,
@@ -71,9 +110,7 @@ const messages = {
71
110
  [subjectCategories.ACTIVE_SUBJECTS]: 'Active',
72
111
  [subjectCategories.ARCHIVE_SUBJECTS]: 'Expired',
73
112
  [subjectCategories.BETA_SUBJECTS]: 'Revised',
74
- [subjectCategories.COMMON_SUBJECTS]: 'Common core subj.',
75
- [subjectCategories.PROGRAMME_SUBJECTS]: 'Programme subj. SF',
76
- [subjectCategories.SPECIALIZED_SUBJECTS]: 'Programme subj. YF',
113
+ [subjectTypes.RESOURCE_COLLECTION]: 'Other resources',
77
114
  },
78
115
  subjectTypes: {
79
116
  [subjectTypes.SUBJECT]: 'Subject',
@@ -1047,10 +1084,10 @@ const messages = {
1047
1084
  deleteAccount: 'Delete My NDLA',
1048
1085
  welcome:
1049
1086
  'Welcome to my NDLA! You can now save your favourite resources from NDLA and organise them in folders with tags',
1050
- read: { our: 'Read our', ours: 'Read our' },
1087
+ read: { read: 'Read our', our: '.' },
1051
1088
  privacy: 'privacy statement',
1052
1089
  privacyLink: 'https://om.ndla.no/gdpr',
1053
- questions: { question: 'Any questions?', ask: 'Ask us in the chat' },
1090
+ questions: { question: 'Any questions?', ask: 'Ask NDLA' },
1054
1091
  wishToDelete: 'Do you wish to delete your account?',
1055
1092
  terms: {
1056
1093
  terms: 'Terms of use',
@@ -1069,11 +1106,11 @@ const messages = {
1069
1106
  },
1070
1107
  folderInfo: {
1071
1108
  title: 'How to organise your favourite resources in folders',
1072
- text: 'You can get to the folder overview by clicking on my folders on the menu to the left. Here you can create new folders and subfolder. You can also create a new folder in the dialogue window that is activated when you click on the heart in a resource',
1109
+ text: 'You can get to the folder overview by clicking on <strong>My folders</strong> on the menu to the left. Here you can create new folders and subfolder. You can also create a new folder in the dialogue window that is activated when you click on the heart in a resource',
1073
1110
  },
1074
1111
  tagInfo: {
1075
1112
  title: 'How to tag your favourite resources',
1076
- text: 'When you save a resource, you will have the option to tag it with a keyword. This tag can be used to find the resource across folders. By selecting my tags on the menu to the left, you will see all the tags your have used. You can also see which resources are tagget with which keyword.',
1113
+ text: 'When you save a resource, you will have the option to tag it with a keyword. This tag can be used to find the resource across folders. By selecting <strong>My tags</strong> on the menu to the left, you will see all the tags your have used. You can also see which resources are tagget with which keyword.',
1077
1114
  },
1078
1115
  },
1079
1116
  resource: {