@stack-spot/portal-components 2.5.2 → 2.6.1
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/CHANGELOG.md +15 -0
- package/dist/components/form/Select/CustomSelect.d.ts +81 -0
- package/dist/components/form/Select/CustomSelect.d.ts.map +1 -0
- package/dist/components/form/Select/CustomSelect.js +178 -0
- package/dist/components/form/Select/CustomSelect.js.map +1 -0
- package/dist/components/form/Select/DetailedSelect.d.ts +65 -0
- package/dist/components/form/Select/DetailedSelect.d.ts.map +1 -0
- package/dist/components/form/Select/DetailedSelect.js +80 -0
- package/dist/components/form/Select/DetailedSelect.js.map +1 -0
- package/dist/components/form/Select/Select.d.ts +47 -0
- package/dist/components/form/Select/Select.d.ts.map +1 -0
- package/dist/components/form/Select/Select.js +62 -0
- package/dist/components/form/Select/Select.js.map +1 -0
- package/dist/components/form/Select/index.d.ts +5 -0
- package/dist/components/form/Select/index.d.ts.map +1 -0
- package/dist/components/form/Select/index.js +5 -0
- package/dist/components/form/Select/index.js.map +1 -0
- package/dist/components/form/Select/styled.d.ts +6 -0
- package/dist/components/form/Select/styled.d.ts.map +1 -0
- package/dist/components/form/Select/styled.js +165 -0
- package/dist/components/form/Select/styled.js.map +1 -0
- package/dist/components/form/Select/types.d.ts +103 -0
- package/dist/components/form/Select/types.d.ts.map +1 -0
- package/dist/components/form/Select/types.js +2 -0
- package/dist/components/form/Select/types.js.map +1 -0
- package/dist/components/form/Select/utils.d.ts +3 -0
- package/dist/components/form/Select/utils.d.ts.map +1 -0
- package/dist/components/form/Select/utils.js +20 -0
- package/dist/components/form/Select/utils.js.map +1 -0
- package/dist/components/notification/NotificationItem.d.ts.map +1 -1
- package/dist/components/notification/NotificationItem.js +2 -2
- package/dist/components/notification/NotificationItem.js.map +1 -1
- package/dist/containers/NotificationsPage.js +1 -1
- package/dist/hooks/keyboard.d.ts +4 -4
- package/dist/hooks/keyboard.d.ts.map +1 -1
- package/dist/hooks/keyboard.js +2 -2
- package/dist/hooks/keyboard.js.map +1 -1
- package/package.json +2 -2
- package/src/components/form/Select/CustomSelect.tsx +232 -0
- package/src/components/form/Select/DetailedSelect.tsx +85 -0
- package/src/components/form/Select/Select.tsx +67 -0
- package/src/components/form/Select/index.ts +4 -0
- package/src/components/form/Select/styled.ts +165 -0
- package/src/components/form/Select/types.ts +112 -0
- package/src/components/form/Select/utils.tsx +28 -0
- package/src/components/notification/NotificationItem.tsx +6 -2
- package/src/hooks/keyboard.tsx +6 -4
- package/dist/components/form/Select.d.ts +0 -69
- package/dist/components/form/Select.d.ts.map +0 -1
- package/dist/components/form/Select.js +0 -162
- package/dist/components/form/Select.js.map +0 -1
- package/src/components/form/Select.tsx +0 -265
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Text } from '@citric/core'
|
|
2
|
+
import { AccessibleLabel, GenericAccessibleLabel } from './types'
|
|
3
|
+
|
|
4
|
+
export function parseLabel(label: AccessibleLabel): GenericAccessibleLabel {
|
|
5
|
+
if (typeof label === 'string') {
|
|
6
|
+
return {
|
|
7
|
+
option: <Text>{label}</Text>,
|
|
8
|
+
text: label,
|
|
9
|
+
selected: <Text className="clipped-text">{label}</Text>,
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
if ('title' in label) {
|
|
13
|
+
return {
|
|
14
|
+
option: (
|
|
15
|
+
<div className="detailed">
|
|
16
|
+
{label.image && <div className="image">{label.image}</div>}
|
|
17
|
+
<div className="text-content">
|
|
18
|
+
<Text className="title">{label.title}</Text>
|
|
19
|
+
{label.description && <Text className="description" appearance="microtext1">{label.description}</Text>}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
text: `${label.title}: ${label.description}`,
|
|
24
|
+
selected: <Text className="clipped-text">{label.title}</Text>,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return label
|
|
28
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box, Button, Flex, IconBox, OneOfColorSchemesWithVariants, Styles, SxProp, Text } from '@citric/core'
|
|
2
|
-
import { Envelope, EnvelopeOpen } from '@citric/icons'
|
|
2
|
+
import { Envelope, EnvelopeOpen, ExternalLink } from '@citric/icons'
|
|
3
3
|
import { IconButton, Tooltip } from '@citric/ui'
|
|
4
4
|
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
5
5
|
import { differenceInDays, parseISO } from 'date-fns'
|
|
@@ -129,15 +129,19 @@ const NotificationFooter = ({ actionURL, onClickAction, title }: NotificationFoo
|
|
|
129
129
|
<Button
|
|
130
130
|
size="sm"
|
|
131
131
|
colorScheme="inverse"
|
|
132
|
+
appearance="text"
|
|
132
133
|
onClick={onClickAction}
|
|
133
134
|
as={Link}
|
|
134
135
|
href={actionURL}
|
|
135
136
|
aria-label={t.viewNotification.replace('%s', title || t.view)}
|
|
136
137
|
target="_blank"
|
|
137
138
|
>
|
|
138
|
-
<Text
|
|
139
|
+
<Text sx={{ mr: 2, textDecoration: 'underline' }}>
|
|
139
140
|
{t.view}
|
|
140
141
|
</Text>
|
|
142
|
+
<IconBox size="xs">
|
|
143
|
+
<ExternalLink />
|
|
144
|
+
</IconBox>
|
|
141
145
|
</Button>
|
|
142
146
|
</Flex>
|
|
143
147
|
)
|
package/src/hooks/keyboard.tsx
CHANGED
|
@@ -18,16 +18,18 @@ interface Props {
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Creates listeners for controlling a Menu UI through the keyboard.
|
|
21
|
-
* - Arrow down: next element in the iterator returned by the query selectors. First element,
|
|
22
|
-
* - Arrow up: previous element in the iterator returned by the query selectors. Last element,
|
|
21
|
+
* - Arrow down: next element in the iterator returned by the query selectors. First element, if the current element is the last.
|
|
22
|
+
* - Arrow up: previous element in the iterator returned by the query selectors. Last element, if the current element is the first.
|
|
23
23
|
* - Tab: same as Arrow down, but has a different behavior if the element is the last (see onPressLastTab).
|
|
24
24
|
* - Esc: determined by onPressEscape.
|
|
25
25
|
* @param props {@link Props}.
|
|
26
26
|
* @returns an object with the element controlled by the keyboard (useRef); a function to attach the keyboard events and a function to
|
|
27
27
|
* detach the keyboard events.
|
|
28
28
|
*/
|
|
29
|
-
export function useKeyboardControls
|
|
30
|
-
|
|
29
|
+
export function useKeyboardControls<T extends HTMLElement = HTMLDivElement>(
|
|
30
|
+
{ querySelectors, onPressEscape, onPressLastTab = onPressEscape }: Props,
|
|
31
|
+
) {
|
|
32
|
+
const keyboardControlledElement = useRef<T>(null)
|
|
31
33
|
const listeners = useRef<Pick<Props, 'onPressEscape' | 'onPressLastTab'>>({})
|
|
32
34
|
listeners.current = { onPressEscape, onPressLastTab }
|
|
33
35
|
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { InputHTMLAttributes } from 'react';
|
|
2
|
-
interface BaseSelectProps<T> extends Omit<InputHTMLAttributes<HTMLSelectElement>, 'value' | 'onChange'> {
|
|
3
|
-
/**
|
|
4
|
-
* The current value.
|
|
5
|
-
*/
|
|
6
|
-
value: T | undefined;
|
|
7
|
-
/**
|
|
8
|
-
* The label for the empty option. This sets the value to undefined.
|
|
9
|
-
*
|
|
10
|
-
* If this is not set, there won't be an empty option for this select.
|
|
11
|
-
*/
|
|
12
|
-
emptyOption?: string;
|
|
13
|
-
/**
|
|
14
|
-
* The options to render in this selection menu.
|
|
15
|
-
*/
|
|
16
|
-
options: T[];
|
|
17
|
-
/**
|
|
18
|
-
* Provides the value of each option. This can be either a key of the option object or a function that receives the option and returns
|
|
19
|
-
* the value.
|
|
20
|
-
*
|
|
21
|
-
* This is required if the options are not strings or numbers.
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* - `'id'`
|
|
25
|
-
* - `(option) => option.id`
|
|
26
|
-
*/
|
|
27
|
-
renderValue?: keyof T | ((item: Exclude<T, undefined>) => string);
|
|
28
|
-
/**
|
|
29
|
-
* Provides the label of each option. This can be either a key of the option object or a function that receives the option and returns
|
|
30
|
-
* the label.
|
|
31
|
-
*
|
|
32
|
-
* This is required if the options are not strings or numbers.
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* - `'name'`
|
|
36
|
-
* - `(option) => option.name`
|
|
37
|
-
*/
|
|
38
|
-
renderLabel?: keyof T | ((item: Exclude<T, undefined>) => string);
|
|
39
|
-
/**
|
|
40
|
-
* Called when the value changes.
|
|
41
|
-
* @param value the new value.
|
|
42
|
-
*/
|
|
43
|
-
onChange: (value: T | undefined) => void;
|
|
44
|
-
/**
|
|
45
|
-
* The maximum number of items before showing a vertical scroll bar.
|
|
46
|
-
* @default 6
|
|
47
|
-
*/
|
|
48
|
-
maxItems?: number;
|
|
49
|
-
}
|
|
50
|
-
interface OptionalSelectProps<T> extends BaseSelectProps<T> {
|
|
51
|
-
emptyOption: string;
|
|
52
|
-
}
|
|
53
|
-
interface RequiredSelectProps<T> extends Omit<BaseSelectProps<T>, 'onChange'> {
|
|
54
|
-
emptyOption?: undefined;
|
|
55
|
-
value: T;
|
|
56
|
-
onChange: (value: T) => void;
|
|
57
|
-
}
|
|
58
|
-
type SelectProps<T> = OptionalSelectProps<T> | RequiredSelectProps<T>;
|
|
59
|
-
/**
|
|
60
|
-
* Renders a Select component using the Citric Design System.
|
|
61
|
-
*
|
|
62
|
-
* The styled version of the select component is rendered on top of the default select from the browser. Visual users will use the Citric
|
|
63
|
-
* version of a Select, but blind users, who interacts with the keyboard, will use the default browser select instead, which is already
|
|
64
|
-
* highly optimized for accessibility.
|
|
65
|
-
* @param props the component props: {@link SelectProps}.
|
|
66
|
-
*/
|
|
67
|
-
export declare function Select<T>({ onChange, options, value, emptyOption, renderLabel, renderValue, maxItems, onFocus, onBlur, style, className, ...props }: SelectProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
68
|
-
export {};
|
|
69
|
-
//# sourceMappingURL=Select.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../../src/components/form/Select.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAqD,MAAM,OAAO,CAAA;AAG9F,UAAU,eAAe,CAAC,CAAC,CAAE,SAAQ,IAAI,CAAC,mBAAmB,CAAC,iBAAiB,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACrG;;OAEG;IACH,KAAK,EAAE,CAAC,GAAG,SAAS,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,OAAO,EAAE,CAAC,EAAE,CAAC;IACb;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,KAAK,MAAM,CAAC,CAAC;IAClE;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,KAAK,MAAM,CAAC,CAAC;IAClE;;;OAGG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC;IACzC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,mBAAmB,CAAC,CAAC,CAAE,SAAQ,eAAe,CAAC,CAAC,CAAC;IACzD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,mBAAmB,CAAC,CAAC,CAAE,SAAQ,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC;IAC3E,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;CAC9B;AAED,KAAK,WAAW,CAAC,CAAC,IAAI,mBAAmB,CAAC,CAAC,CAAC,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAAA;AAkHrE;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,EACxB,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,QAAY,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,KAAK,EAC3H,EAAE,WAAW,CAAC,CAAC,CAAC,2CA2EhB"}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { IconBox, Text } from '@citric/core';
|
|
3
|
-
import { ChevronDown } from '@citric/icons';
|
|
4
|
-
import { listToClass, theme } from '@stack-spot/portal-theme';
|
|
5
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
-
import { styled } from 'styled-components';
|
|
7
|
-
const OPTION_HEIGHT = 32;
|
|
8
|
-
const LIST_BOTTOM_PADDING = 7;
|
|
9
|
-
const SelectBox = styled.div `
|
|
10
|
-
position: relative;
|
|
11
|
-
|
|
12
|
-
select {
|
|
13
|
-
border: none;
|
|
14
|
-
height: 40px;
|
|
15
|
-
opacity: 0;
|
|
16
|
-
pointer-events: none;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
.fake-select {
|
|
20
|
-
position: absolute;
|
|
21
|
-
top: 0;
|
|
22
|
-
left: 0;
|
|
23
|
-
right: 0;
|
|
24
|
-
border-radius: 0.25rem;
|
|
25
|
-
display: flex;
|
|
26
|
-
flex-direction: column;
|
|
27
|
-
border: 1px solid ${theme.color.light[600]};
|
|
28
|
-
transition: border-color 0.3s, box-shadow 0.3s;
|
|
29
|
-
z-index: 1;
|
|
30
|
-
background-color: ${theme.color.light[300]};
|
|
31
|
-
|
|
32
|
-
.arrow {
|
|
33
|
-
transition: transform ease-in-out 0.3s;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
&.focused, &.open {
|
|
37
|
-
border: 1px solid ${theme.color.primary[500]};
|
|
38
|
-
box-shadow: 0 0 0 1px ${theme.color.primary[500]};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
&.open {
|
|
42
|
-
.arrow {
|
|
43
|
-
transform: rotate(180deg);
|
|
44
|
-
}
|
|
45
|
-
.options {
|
|
46
|
-
/* lets the overflow be hidden until the animation on the height ends. */
|
|
47
|
-
overflow-y: auto;
|
|
48
|
-
animation: 0.3s overflow-animation;
|
|
49
|
-
@keyframes overflow-animation {
|
|
50
|
-
0% {
|
|
51
|
-
overflow-y: hidden;
|
|
52
|
-
}
|
|
53
|
-
99% {
|
|
54
|
-
overflow-y: hidden;
|
|
55
|
-
}
|
|
56
|
-
100% {
|
|
57
|
-
overflow-y: auto;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.current-value {
|
|
64
|
-
height: 40px;
|
|
65
|
-
display: flex;
|
|
66
|
-
flex-direction: row;
|
|
67
|
-
padding: 0 8px;
|
|
68
|
-
justify-content: space-between;
|
|
69
|
-
align-items: center;
|
|
70
|
-
cursor: pointer;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.clipped-text {
|
|
74
|
-
text-overflow: ellipsis;
|
|
75
|
-
width: 100%;
|
|
76
|
-
overflow: hidden;
|
|
77
|
-
white-space: nowrap;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.options {
|
|
81
|
-
list-style: none;
|
|
82
|
-
padding: 0;
|
|
83
|
-
margin: 0;
|
|
84
|
-
overflow-y: hidden;
|
|
85
|
-
transition: height ease-in-out 0.3s;
|
|
86
|
-
max-height: ${({ $maxItems }) => $maxItems * OPTION_HEIGHT + LIST_BOTTOM_PADDING}px;
|
|
87
|
-
|
|
88
|
-
li {
|
|
89
|
-
height: ${OPTION_HEIGHT}px;
|
|
90
|
-
display: flex;
|
|
91
|
-
flex-direction: row;
|
|
92
|
-
align-items: center;
|
|
93
|
-
padding: 0 8px;
|
|
94
|
-
border-top: 1px solid ${theme.color.light[600]};
|
|
95
|
-
cursor: pointer;
|
|
96
|
-
transition: background-color 0.2s;
|
|
97
|
-
&:hover {
|
|
98
|
-
background-color: ${theme.color.light[500]};
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
`;
|
|
104
|
-
function renderProperty(option, renderer) {
|
|
105
|
-
if (!renderer)
|
|
106
|
-
return `${option ?? ''}`;
|
|
107
|
-
return typeof renderer === 'function' ? renderer(option) : (option[renderer] ?? `${option ?? ''}`);
|
|
108
|
-
}
|
|
109
|
-
const FakeOption = ({ value, label, onChange }) => (_jsx("li", { className: "option", onClick: () => onChange({ target: { value } }), children: _jsx(Text, { className: "clipped-text", children: label }) }));
|
|
110
|
-
/**
|
|
111
|
-
* Renders a Select component using the Citric Design System.
|
|
112
|
-
*
|
|
113
|
-
* The styled version of the select component is rendered on top of the default select from the browser. Visual users will use the Citric
|
|
114
|
-
* version of a Select, but blind users, who interacts with the keyboard, will use the default browser select instead, which is already
|
|
115
|
-
* highly optimized for accessibility.
|
|
116
|
-
* @param props the component props: {@link SelectProps}.
|
|
117
|
-
*/
|
|
118
|
-
export function Select({ onChange, options, value, emptyOption, renderLabel, renderValue, maxItems = 6, onFocus, onBlur, style, className, ...props }) {
|
|
119
|
-
const [open, setOpen] = useState(false);
|
|
120
|
-
const [focused, setFocused] = useState(false);
|
|
121
|
-
const fakeSelectRef = useRef(null);
|
|
122
|
-
const onChangeOption = useCallback((event) => {
|
|
123
|
-
const value = options.find(o => renderProperty(o, renderValue) === event.target.value);
|
|
124
|
-
onChange(value);
|
|
125
|
-
setOpen(false);
|
|
126
|
-
}, []);
|
|
127
|
-
const onClickOutside = useCallback((event) => {
|
|
128
|
-
if (fakeSelectRef.current && !fakeSelectRef.current.contains(event.target))
|
|
129
|
-
setOpen(false);
|
|
130
|
-
}, []);
|
|
131
|
-
const [htmlOptions, fakeOptions] = useMemo(() => options.reduce(([opts, fake], o) => {
|
|
132
|
-
const id = renderProperty(o, renderValue);
|
|
133
|
-
const label = renderProperty(o, renderLabel);
|
|
134
|
-
return [
|
|
135
|
-
[...opts, _jsx("option", { value: id, selected: value === id, children: label }, id)],
|
|
136
|
-
[...fake, _jsx(FakeOption, { value: id, label: label, onChange: onChangeOption }, id)],
|
|
137
|
-
];
|
|
138
|
-
}, [[], []]), [options, value]);
|
|
139
|
-
const height = open ? (LIST_BOTTOM_PADDING + (options.length + (emptyOption === undefined ? 0 : 1)) * OPTION_HEIGHT) : 0;
|
|
140
|
-
function getCurrentValue() {
|
|
141
|
-
return value === undefined ? '' : renderProperty(value, renderValue);
|
|
142
|
-
}
|
|
143
|
-
function getCurrentLabel() {
|
|
144
|
-
return value === undefined ? (emptyOption ?? '') : renderProperty(value, renderLabel);
|
|
145
|
-
}
|
|
146
|
-
useEffect(() => {
|
|
147
|
-
const detach = () => document.removeEventListener('mousedown', onClickOutside);
|
|
148
|
-
if (open)
|
|
149
|
-
document.addEventListener('mousedown', onClickOutside);
|
|
150
|
-
else
|
|
151
|
-
detach();
|
|
152
|
-
return detach;
|
|
153
|
-
}, [open]);
|
|
154
|
-
return (_jsxs(SelectBox, { style: style, className: className, "$maxItems": maxItems, children: [_jsxs("select", { ...props, value: getCurrentValue(), onChange: onChangeOption, onFocus: (ev) => {
|
|
155
|
-
setFocused(true);
|
|
156
|
-
onFocus?.(ev);
|
|
157
|
-
}, onBlur: (ev) => {
|
|
158
|
-
setFocused(false);
|
|
159
|
-
onBlur?.(ev);
|
|
160
|
-
}, children: [emptyOption === undefined ? null : _jsx("option", { value: "", selected: !value, children: emptyOption }), htmlOptions] }), _jsxs("div", { ref: fakeSelectRef, className: listToClass(['fake-select', open && 'open', focused && 'focused']), "aria-hidden": true, children: [_jsxs("div", { className: "current-value", onClick: () => setOpen(!open), children: [_jsx(Text, { className: "clipped-text", children: getCurrentLabel() }), _jsx(IconBox, { className: "arrow", children: _jsx(ChevronDown, {}) })] }), _jsxs("ul", { className: "options", style: { height: `${height}px` }, children: [emptyOption === undefined ? null : _jsx(FakeOption, { value: "", label: emptyOption, onChange: onChangeOption }), fakeOptions] })] })] }));
|
|
161
|
-
}
|
|
162
|
-
//# sourceMappingURL=Select.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Select.js","sourceRoot":"","sources":["../../../src/components/form/Select.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import { IconBox, Text } from '@citric/core'
|
|
2
|
-
import { ChevronDown } from '@citric/icons'
|
|
3
|
-
import { listToClass, theme } from '@stack-spot/portal-theme'
|
|
4
|
-
import { InputHTMLAttributes, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
5
|
-
import { styled } from 'styled-components'
|
|
6
|
-
|
|
7
|
-
interface BaseSelectProps<T> extends Omit<InputHTMLAttributes<HTMLSelectElement>, 'value' | 'onChange'> {
|
|
8
|
-
/**
|
|
9
|
-
* The current value.
|
|
10
|
-
*/
|
|
11
|
-
value: T | undefined,
|
|
12
|
-
/**
|
|
13
|
-
* The label for the empty option. This sets the value to undefined.
|
|
14
|
-
*
|
|
15
|
-
* If this is not set, there won't be an empty option for this select.
|
|
16
|
-
*/
|
|
17
|
-
emptyOption?: string,
|
|
18
|
-
/**
|
|
19
|
-
* The options to render in this selection menu.
|
|
20
|
-
*/
|
|
21
|
-
options: T[],
|
|
22
|
-
/**
|
|
23
|
-
* Provides the value of each option. This can be either a key of the option object or a function that receives the option and returns
|
|
24
|
-
* the value.
|
|
25
|
-
*
|
|
26
|
-
* This is required if the options are not strings or numbers.
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* - `'id'`
|
|
30
|
-
* - `(option) => option.id`
|
|
31
|
-
*/
|
|
32
|
-
renderValue?: keyof T | ((item: Exclude<T, undefined>) => string),
|
|
33
|
-
/**
|
|
34
|
-
* Provides the label of each option. This can be either a key of the option object or a function that receives the option and returns
|
|
35
|
-
* the label.
|
|
36
|
-
*
|
|
37
|
-
* This is required if the options are not strings or numbers.
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* - `'name'`
|
|
41
|
-
* - `(option) => option.name`
|
|
42
|
-
*/
|
|
43
|
-
renderLabel?: keyof T | ((item: Exclude<T, undefined>) => string),
|
|
44
|
-
/**
|
|
45
|
-
* Called when the value changes.
|
|
46
|
-
* @param value the new value.
|
|
47
|
-
*/
|
|
48
|
-
onChange: (value: T | undefined) => void,
|
|
49
|
-
/**
|
|
50
|
-
* The maximum number of items before showing a vertical scroll bar.
|
|
51
|
-
* @default 6
|
|
52
|
-
*/
|
|
53
|
-
maxItems?: number,
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface OptionalSelectProps<T> extends BaseSelectProps<T> {
|
|
57
|
-
emptyOption: string,
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
interface RequiredSelectProps<T> extends Omit<BaseSelectProps<T>, 'onChange'> {
|
|
61
|
-
emptyOption?: undefined,
|
|
62
|
-
value: T,
|
|
63
|
-
onChange: (value: T) => void,
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
type SelectProps<T> = OptionalSelectProps<T> | RequiredSelectProps<T>
|
|
67
|
-
|
|
68
|
-
const OPTION_HEIGHT = 32
|
|
69
|
-
const LIST_BOTTOM_PADDING = 7
|
|
70
|
-
|
|
71
|
-
const SelectBox = styled.div<{ $maxItems: number }>`
|
|
72
|
-
position: relative;
|
|
73
|
-
|
|
74
|
-
select {
|
|
75
|
-
border: none;
|
|
76
|
-
height: 40px;
|
|
77
|
-
opacity: 0;
|
|
78
|
-
pointer-events: none;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.fake-select {
|
|
82
|
-
position: absolute;
|
|
83
|
-
top: 0;
|
|
84
|
-
left: 0;
|
|
85
|
-
right: 0;
|
|
86
|
-
border-radius: 0.25rem;
|
|
87
|
-
display: flex;
|
|
88
|
-
flex-direction: column;
|
|
89
|
-
border: 1px solid ${theme.color.light[600]};
|
|
90
|
-
transition: border-color 0.3s, box-shadow 0.3s;
|
|
91
|
-
z-index: 1;
|
|
92
|
-
background-color: ${theme.color.light[300]};
|
|
93
|
-
|
|
94
|
-
.arrow {
|
|
95
|
-
transition: transform ease-in-out 0.3s;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
&.focused, &.open {
|
|
99
|
-
border: 1px solid ${theme.color.primary[500]};
|
|
100
|
-
box-shadow: 0 0 0 1px ${theme.color.primary[500]};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
&.open {
|
|
104
|
-
.arrow {
|
|
105
|
-
transform: rotate(180deg);
|
|
106
|
-
}
|
|
107
|
-
.options {
|
|
108
|
-
/* lets the overflow be hidden until the animation on the height ends. */
|
|
109
|
-
overflow-y: auto;
|
|
110
|
-
animation: 0.3s overflow-animation;
|
|
111
|
-
@keyframes overflow-animation {
|
|
112
|
-
0% {
|
|
113
|
-
overflow-y: hidden;
|
|
114
|
-
}
|
|
115
|
-
99% {
|
|
116
|
-
overflow-y: hidden;
|
|
117
|
-
}
|
|
118
|
-
100% {
|
|
119
|
-
overflow-y: auto;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.current-value {
|
|
126
|
-
height: 40px;
|
|
127
|
-
display: flex;
|
|
128
|
-
flex-direction: row;
|
|
129
|
-
padding: 0 8px;
|
|
130
|
-
justify-content: space-between;
|
|
131
|
-
align-items: center;
|
|
132
|
-
cursor: pointer;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
.clipped-text {
|
|
136
|
-
text-overflow: ellipsis;
|
|
137
|
-
width: 100%;
|
|
138
|
-
overflow: hidden;
|
|
139
|
-
white-space: nowrap;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
.options {
|
|
143
|
-
list-style: none;
|
|
144
|
-
padding: 0;
|
|
145
|
-
margin: 0;
|
|
146
|
-
overflow-y: hidden;
|
|
147
|
-
transition: height ease-in-out 0.3s;
|
|
148
|
-
max-height: ${({ $maxItems }) => $maxItems * OPTION_HEIGHT + LIST_BOTTOM_PADDING}px;
|
|
149
|
-
|
|
150
|
-
li {
|
|
151
|
-
height: ${OPTION_HEIGHT}px;
|
|
152
|
-
display: flex;
|
|
153
|
-
flex-direction: row;
|
|
154
|
-
align-items: center;
|
|
155
|
-
padding: 0 8px;
|
|
156
|
-
border-top: 1px solid ${theme.color.light[600]};
|
|
157
|
-
cursor: pointer;
|
|
158
|
-
transition: background-color 0.2s;
|
|
159
|
-
&:hover {
|
|
160
|
-
background-color: ${theme.color.light[500]};
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
`
|
|
166
|
-
|
|
167
|
-
function renderProperty(option: any, renderer: any): string {
|
|
168
|
-
if (!renderer) return `${option ?? ''}`
|
|
169
|
-
return typeof renderer === 'function' ? renderer(option) : (option[renderer] ?? `${option ?? ''}`)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const FakeOption = (
|
|
173
|
-
{ value, label, onChange }: { value: string, label: string, onChange: (event: { target: { value: string } }) => void },
|
|
174
|
-
) => (
|
|
175
|
-
<li className="option" onClick={() => onChange({ target: { value } })}>
|
|
176
|
-
<Text className="clipped-text">{label}</Text>
|
|
177
|
-
</li>
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Renders a Select component using the Citric Design System.
|
|
182
|
-
*
|
|
183
|
-
* The styled version of the select component is rendered on top of the default select from the browser. Visual users will use the Citric
|
|
184
|
-
* version of a Select, but blind users, who interacts with the keyboard, will use the default browser select instead, which is already
|
|
185
|
-
* highly optimized for accessibility.
|
|
186
|
-
* @param props the component props: {@link SelectProps}.
|
|
187
|
-
*/
|
|
188
|
-
export function Select<T>({
|
|
189
|
-
onChange, options, value, emptyOption, renderLabel, renderValue, maxItems = 6, onFocus, onBlur, style, className, ...props
|
|
190
|
-
}: SelectProps<T>) {
|
|
191
|
-
const [open, setOpen] = useState(false)
|
|
192
|
-
const [focused, setFocused] = useState(false)
|
|
193
|
-
const fakeSelectRef = useRef<HTMLDivElement>(null)
|
|
194
|
-
|
|
195
|
-
const onChangeOption = useCallback((event: { target: { value: string } }) => {
|
|
196
|
-
const value = options.find(o => renderProperty(o, renderValue) === event.target.value)
|
|
197
|
-
onChange(value!)
|
|
198
|
-
setOpen(false)
|
|
199
|
-
}, [])
|
|
200
|
-
|
|
201
|
-
const onClickOutside = useCallback((event: MouseEvent) => {
|
|
202
|
-
if (fakeSelectRef.current && !fakeSelectRef.current.contains(event.target as Node)) setOpen(false)
|
|
203
|
-
}, [])
|
|
204
|
-
|
|
205
|
-
const [htmlOptions, fakeOptions] = useMemo(
|
|
206
|
-
() => options.reduce<[React.ReactElement[], React.ReactElement[]]>(([opts, fake], o) => {
|
|
207
|
-
const id = renderProperty(o, renderValue)
|
|
208
|
-
const label = renderProperty(o, renderLabel)
|
|
209
|
-
return [
|
|
210
|
-
[...opts, <option key={id} value={id} selected={value === id}>{label}</option>],
|
|
211
|
-
[...fake, <FakeOption key={id} value={id} label={label} onChange={onChangeOption} />],
|
|
212
|
-
]
|
|
213
|
-
}, [[], []]),
|
|
214
|
-
[options, value],
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
const height = open ? (LIST_BOTTOM_PADDING + (options.length + (emptyOption === undefined ? 0 : 1)) * OPTION_HEIGHT) : 0
|
|
218
|
-
|
|
219
|
-
function getCurrentValue() {
|
|
220
|
-
return value === undefined ? '' : renderProperty(value, renderValue)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function getCurrentLabel() {
|
|
224
|
-
return value === undefined ? (emptyOption ?? '') : renderProperty(value, renderLabel)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
useEffect(() => {
|
|
228
|
-
const detach = () => document.removeEventListener('mousedown', onClickOutside)
|
|
229
|
-
if (open) document.addEventListener('mousedown', onClickOutside)
|
|
230
|
-
else detach()
|
|
231
|
-
return detach
|
|
232
|
-
}, [open])
|
|
233
|
-
|
|
234
|
-
return (
|
|
235
|
-
<SelectBox style={style} className={className} $maxItems={maxItems}>
|
|
236
|
-
{ /* Screen readers can use the select component from the browser instead of the highly styled component we show. */ }
|
|
237
|
-
<select
|
|
238
|
-
{...props}
|
|
239
|
-
value={getCurrentValue()}
|
|
240
|
-
onChange={onChangeOption}
|
|
241
|
-
onFocus={(ev) => {
|
|
242
|
-
setFocused(true)
|
|
243
|
-
onFocus?.(ev)
|
|
244
|
-
}}
|
|
245
|
-
onBlur={(ev) => {
|
|
246
|
-
setFocused(false)
|
|
247
|
-
onBlur?.(ev)
|
|
248
|
-
}}
|
|
249
|
-
>
|
|
250
|
-
{emptyOption === undefined ? null : <option value="" selected={!value}>{emptyOption}</option>}
|
|
251
|
-
{htmlOptions}
|
|
252
|
-
</select>
|
|
253
|
-
<div ref={fakeSelectRef} className={listToClass(['fake-select', open && 'open', focused && 'focused'])} aria-hidden>
|
|
254
|
-
<div className="current-value" onClick={() => setOpen(!open)}>
|
|
255
|
-
<Text className="clipped-text">{getCurrentLabel()}</Text>
|
|
256
|
-
<IconBox className="arrow"><ChevronDown /></IconBox>
|
|
257
|
-
</div>
|
|
258
|
-
<ul className="options" style={{ height: `${height}px` }}>
|
|
259
|
-
{emptyOption === undefined ? null : <FakeOption value="" label={emptyOption} onChange={onChangeOption} />}
|
|
260
|
-
{fakeOptions}
|
|
261
|
-
</ul>
|
|
262
|
-
</div>
|
|
263
|
-
</SelectBox>
|
|
264
|
-
)
|
|
265
|
-
}
|