@ndla/ui 28.0.0 → 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.
- package/es/Resource/ListResource.js +8 -8
- package/es/TagSelector/Control.js +23 -0
- package/es/TagSelector/DropdownIndicator.js +66 -0
- package/es/TagSelector/Input.js +19 -0
- package/es/TagSelector/Menu.js +26 -0
- package/es/TagSelector/MenuList.js +22 -0
- package/es/TagSelector/Option.js +55 -0
- package/es/TagSelector/SelectContainer.js +18 -0
- package/es/TagSelector/TagSelector.js +161 -100
- package/es/TagSelector/ValueButton.js +46 -0
- package/es/TagSelector/ariaMessages.js +104 -0
- package/es/TagSelector/index.js +2 -1
- package/es/TagSelector/types.js +0 -0
- package/es/TreeStructure/ComboboxButton.js +19 -18
- package/es/TreeStructure/TreeStructure.js +8 -8
- package/es/locale/messages-en.js +40 -2
- package/es/locale/messages-nb.js +40 -2
- package/es/locale/messages-nn.js +40 -2
- package/es/locale/messages-se.js +40 -2
- package/es/locale/messages-sma.js +40 -2
- package/lib/Resource/ListResource.js +8 -8
- package/lib/TagSelector/Control.d.ts +12 -0
- package/lib/TagSelector/Control.js +35 -0
- package/lib/TagSelector/DropdownIndicator.d.ts +12 -0
- package/lib/TagSelector/DropdownIndicator.js +80 -0
- package/lib/TagSelector/Input.d.ts +12 -0
- package/lib/TagSelector/Input.js +33 -0
- package/lib/TagSelector/Menu.d.ts +12 -0
- package/lib/TagSelector/Menu.js +40 -0
- package/lib/TagSelector/MenuList.d.ts +13 -0
- package/lib/TagSelector/MenuList.js +36 -0
- package/lib/TagSelector/Option.d.ts +12 -0
- package/lib/TagSelector/Option.js +61 -0
- package/lib/TagSelector/SelectContainer.d.ts +12 -0
- package/lib/TagSelector/SelectContainer.js +31 -0
- package/lib/TagSelector/TagSelector.d.ts +14 -11
- package/lib/TagSelector/TagSelector.js +165 -96
- package/lib/TagSelector/ValueButton.d.ts +16 -0
- package/lib/TagSelector/ValueButton.js +55 -0
- package/lib/TagSelector/ariaMessages.d.ts +16 -0
- package/lib/TagSelector/ariaMessages.js +113 -0
- package/lib/TagSelector/index.d.ts +2 -1
- package/lib/TagSelector/index.js +3 -5
- package/lib/TagSelector/types.d.ts +11 -0
- package/lib/TagSelector/types.js +1 -0
- package/lib/TreeStructure/ComboboxButton.js +19 -18
- package/lib/TreeStructure/TreeStructure.js +7 -7
- package/lib/locale/messages-en.d.ts +40 -2
- package/lib/locale/messages-en.js +40 -2
- package/lib/locale/messages-nb.d.ts +40 -2
- package/lib/locale/messages-nb.js +40 -2
- package/lib/locale/messages-nn.d.ts +40 -2
- package/lib/locale/messages-nn.js +40 -2
- package/lib/locale/messages-se.d.ts +40 -2
- package/lib/locale/messages-se.js +40 -2
- package/lib/locale/messages-sma.d.ts +40 -2
- package/lib/locale/messages-sma.js +40 -2
- package/package.json +15 -14
- package/src/Resource/ListResource.tsx +1 -1
- package/src/TagSelector/Control.tsx +34 -0
- package/src/TagSelector/DropdownIndicator.tsx +47 -0
- package/src/TagSelector/Input.tsx +31 -0
- package/src/TagSelector/Menu.tsx +38 -0
- package/src/TagSelector/MenuList.tsx +30 -0
- package/src/TagSelector/Option.tsx +53 -0
- package/src/TagSelector/SelectContainer.tsx +32 -0
- package/src/TagSelector/TagSelector.tsx +105 -84
- package/src/TagSelector/ValueButton.tsx +46 -0
- package/src/TagSelector/ariaMessages.ts +87 -0
- package/src/TagSelector/index.ts +2 -1
- package/src/TagSelector/types.ts +12 -0
- package/src/TreeStructure/ComboboxButton.tsx +15 -17
- package/src/TreeStructure/TreeStructure.tsx +2 -11
- package/src/locale/messages-en.ts +41 -2
- package/src/locale/messages-nb.ts +41 -2
- package/src/locale/messages-nn.ts +41 -2
- package/src/locale/messages-se.ts +41 -2
- package/src/locale/messages-sma.ts +41 -2
- package/es/TagSelector/SuggestionInput.js +0 -285
- package/es/TagSelector/Suggestions.js +0 -97
- package/lib/TagSelector/SuggestionInput.d.ts +0 -19
- package/lib/TagSelector/SuggestionInput.js +0 -299
- package/lib/TagSelector/Suggestions.d.ts +0 -12
- package/lib/TagSelector/Suggestions.js +0 -99
- package/src/TagSelector/SuggestionInput.tsx +0 -287
- package/src/TagSelector/Suggestions.tsx +0 -139
|
@@ -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, {
|
|
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 {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
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
|
|
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
|
|
18
|
-
|
|
45
|
+
const StyledTagSelector = styled.div`
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
flex: 1;
|
|
49
|
+
overflow: hidden;
|
|
19
50
|
`;
|
|
20
51
|
|
|
21
|
-
|
|
22
|
-
|
|
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:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
onCreateTag: (
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
</
|
|
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
|
+
});
|
package/src/TagSelector/index.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
75
|
+
hideTags: 'Hide tags',
|
|
76
|
+
showTags: 'Show tags',
|
|
38
77
|
},
|
|
39
78
|
htmlTitles: {
|
|
40
79
|
titleTemplate,
|
|
@@ -30,11 +30,50 @@ const messages = {
|
|
|
30
30
|
},
|
|
31
31
|
},
|
|
32
32
|
tagSelector: {
|
|
33
|
+
aria: {
|
|
34
|
+
screenReaderStatus: '{{count}} resultater tilgjengelig',
|
|
35
|
+
disabled: 'utilgjengelig',
|
|
36
|
+
selected: 'valgt',
|
|
37
|
+
focused: 'fokusert',
|
|
38
|
+
guidance: {
|
|
39
|
+
menu: {
|
|
40
|
+
updown: 'Bruk piltaster opp og ned for å velge emneknagger',
|
|
41
|
+
enter: 'trykk enter for å velge den markerte emneknaggen',
|
|
42
|
+
escape: 'trykk escape for å lukke menyen',
|
|
43
|
+
tab: 'trykk tab for å velge emneknaggen og lukke menyen',
|
|
44
|
+
},
|
|
45
|
+
input: {
|
|
46
|
+
select: 'Emneknagg-meny',
|
|
47
|
+
focused: 'er fokusert',
|
|
48
|
+
refine: 'skriv for å filtrere listen med emneknagger',
|
|
49
|
+
down: 'trykk pil ned for å åpne menyen',
|
|
50
|
+
left: 'trykk venstre pil for å fokusere valgte emneknagger',
|
|
51
|
+
space: 'trykk mellomrom for å opprette ny emneknagg',
|
|
52
|
+
},
|
|
53
|
+
value:
|
|
54
|
+
'Bruk høyre og venstre pil for å navigere mellom valgte emneknagger, trykk backspace for å fjerne den valgte emneknaggen. Dersom ingen emneknagg er valgt fjernes den siste.',
|
|
55
|
+
},
|
|
56
|
+
onChange: {
|
|
57
|
+
deselect: 'emneknagg {{label}}, fjernet.',
|
|
58
|
+
clear: 'Alle valgte emneknagger fjernet.',
|
|
59
|
+
initialFocus: `Emneknagger {{labels}}, valgt.`,
|
|
60
|
+
selectedDisabled: 'Emneknagg {{label}} kan ikke velges. Velg et annet alternativ.',
|
|
61
|
+
selected: 'Emneknagg {{label}}, valgt.',
|
|
62
|
+
},
|
|
63
|
+
onFocus: {
|
|
64
|
+
value: 'emneknagg {{label}} fokusert, {{position}}.',
|
|
65
|
+
menu: 'emneknagg {{label}} {{status}}, {{position}}.',
|
|
66
|
+
of: 'av',
|
|
67
|
+
},
|
|
68
|
+
onFilter: ' for søkeord ',
|
|
69
|
+
},
|
|
70
|
+
noOptions: 'Ingen valgmuligheter',
|
|
33
71
|
label: 'Legg til emneknagg',
|
|
72
|
+
createLabel: 'Legg til emneknagg {{tag}}',
|
|
34
73
|
placeholder: 'Skriv inn emneknagg',
|
|
35
74
|
removeTag: 'Ta vekk {{name}}',
|
|
36
|
-
|
|
37
|
-
|
|
75
|
+
hideTags: 'Skjul emneknagger',
|
|
76
|
+
showTags: 'Vis emneknagger',
|
|
38
77
|
},
|
|
39
78
|
htmlTitles: {
|
|
40
79
|
titleTemplate,
|
|
@@ -30,11 +30,50 @@ const messages = {
|
|
|
30
30
|
},
|
|
31
31
|
},
|
|
32
32
|
tagSelector: {
|
|
33
|
+
aria: {
|
|
34
|
+
screenReaderStatus: '{{count}} resultat tilgjengeleg',
|
|
35
|
+
disabled: 'utilgjengeleg',
|
|
36
|
+
selected: 'valgt',
|
|
37
|
+
focused: 'fokusert',
|
|
38
|
+
guidance: {
|
|
39
|
+
menu: {
|
|
40
|
+
updown: 'Bruk piltastar opp og ned for å velge emneknaggar',
|
|
41
|
+
enter: 'trykk enter for å velge den markerte emneknaggen',
|
|
42
|
+
escape: 'trykk escape for å lukke menyen',
|
|
43
|
+
tab: 'trykk tab for å velge emneknaggen og lukke menyen',
|
|
44
|
+
},
|
|
45
|
+
input: {
|
|
46
|
+
select: 'Emneknagg-meny',
|
|
47
|
+
focused: 'er fokusert',
|
|
48
|
+
refine: 'skriv for å filtrere lista med emneknaggar',
|
|
49
|
+
down: 'trykk pil ned for å åpne menyen',
|
|
50
|
+
left: 'trykk venstre pil for å fokusere valgte emneknaggar',
|
|
51
|
+
space: 'trykk mellomrom for å opprette ny emneknagg',
|
|
52
|
+
},
|
|
53
|
+
value:
|
|
54
|
+
'Bruk høgre og venstre pil for å navigere mellom valgte emneknaggar, trykk backspace for å fjerne den valgte emneknaggen. Dersom ingen emneknagg er valgt fjernes den siste.',
|
|
55
|
+
},
|
|
56
|
+
onChange: {
|
|
57
|
+
deselect: 'emneknagg {{label}}, fjerna.',
|
|
58
|
+
clear: 'Alle valgte emneknaggar fjerna.',
|
|
59
|
+
initialFocus: `Emneknaggar {{labels}}, valgt.`,
|
|
60
|
+
selectedDisabled: 'Emneknagg {{label}} kan ikkje velgast. Velg eit anna alternativ.',
|
|
61
|
+
selected: 'Emneknagg {{label}}, valgt.',
|
|
62
|
+
},
|
|
63
|
+
onFocus: {
|
|
64
|
+
value: 'emneknagg {{label}} fokusert, {{position}}.',
|
|
65
|
+
menu: 'emneknagg {{label}} {{status}}, {{position}}.',
|
|
66
|
+
of: 'av',
|
|
67
|
+
},
|
|
68
|
+
onFilter: ' for søkeord ',
|
|
69
|
+
},
|
|
70
|
+
noOptions: 'Ingen mulige val',
|
|
33
71
|
label: 'Legg til emneknagg',
|
|
72
|
+
createLabel: 'Legg til emneknagg {{tag}}',
|
|
34
73
|
placeholder: 'Skriv inn emneknagg',
|
|
35
74
|
removeTag: 'Ta vekk {{name}}',
|
|
36
|
-
|
|
37
|
-
|
|
75
|
+
hideTags: 'Skjul emneknaggar',
|
|
76
|
+
showTags: 'Vis emneknaggar',
|
|
38
77
|
},
|
|
39
78
|
htmlTitles: {
|
|
40
79
|
titleTemplate,
|