@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.
- package/es/Frontpage/FrontpageAllSubjects.js +9 -8
- package/es/Resource/BlockResource.js +22 -13
- package/es/Resource/ListResource.js +24 -15
- package/es/Resource/resourceComponents.js +28 -28
- 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 +46 -8
- package/es/locale/messages-nb.js +57 -19
- package/es/locale/messages-nn.js +56 -18
- package/es/locale/messages-se.js +48 -10
- package/es/locale/messages-sma.js +57 -19
- package/es/model/ContentType.js +23 -1
- package/es/model/SubjectCategories.js +1 -5
- package/es/model/index.js +3 -2
- package/lib/Frontpage/FrontpageAllSubjects.js +10 -8
- package/lib/Resource/BlockResource.d.ts +6 -3
- package/lib/Resource/BlockResource.js +23 -12
- package/lib/Resource/ListResource.d.ts +6 -3
- package/lib/Resource/ListResource.js +25 -14
- package/lib/Resource/resourceComponents.d.ts +6 -3
- package/lib/Resource/resourceComponents.js +30 -30
- 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 +56 -25
- package/lib/locale/messages-en.js +46 -8
- package/lib/locale/messages-nb.d.ts +56 -25
- package/lib/locale/messages-nb.js +57 -19
- package/lib/locale/messages-nn.d.ts +56 -25
- package/lib/locale/messages-nn.js +56 -18
- package/lib/locale/messages-se.d.ts +56 -25
- package/lib/locale/messages-se.js +48 -10
- package/lib/locale/messages-sma.d.ts +56 -25
- package/lib/locale/messages-sma.js +57 -19
- package/lib/model/ContentType.d.ts +18 -0
- package/lib/model/ContentType.js +32 -2
- package/lib/model/SubjectCategories.d.ts +0 -3
- package/lib/model/SubjectCategories.js +3 -10
- package/lib/model/index.d.ts +12 -2
- package/lib/model/index.js +4 -3
- package/package.json +15 -14
- package/src/Frontpage/FrontpageAllSubjects.tsx +5 -2
- package/src/Resource/BlockResource.tsx +18 -11
- package/src/Resource/ListResource.tsx +14 -11
- package/src/Resource/resourceComponents.tsx +13 -14
- 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 +46 -9
- package/src/locale/messages-nb.ts +57 -20
- package/src/locale/messages-nn.ts +56 -19
- package/src/locale/messages-se.ts +48 -11
- package/src/locale/messages-sma.ts +57 -20
- package/src/model/ContentType.ts +29 -0
- package/src/model/SubjectCategories.ts +0 -5
- package/src/model/index.ts +2 -1
- 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/.DS_Store +0 -0
- package/src/TagSelector/SuggestionInput.tsx +0 -287
- 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, {
|
|
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,
|
|
@@ -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
|
-
[
|
|
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: {
|
|
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
|
|
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
|
|
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
|
|
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: {
|