@xqmsg/ui-core 0.15.4 → 0.16.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/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.15.4",
2
+ "version": "0.16.1",
3
3
  "license": "MIT",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
@@ -31,6 +31,38 @@ const meta: Meta<BannerProps> = {
31
31
  export default meta;
32
32
  const Template: Story<BannerProps> = args => (
33
33
  <>
34
+ <Box mb="20px">
35
+ <Banner
36
+ {...args}
37
+ type="condensed"
38
+ variant="positive"
39
+ message="Positive message."
40
+ />
41
+ </Box>
42
+ <Box mb="20px">
43
+ <Banner
44
+ {...args}
45
+ type="condensed"
46
+ variant="warning"
47
+ message="Warning message."
48
+ />
49
+ </Box>
50
+ <Box mb="20px">
51
+ <Banner
52
+ {...args}
53
+ type="condensed"
54
+ variant="error"
55
+ message="Error message."
56
+ />
57
+ </Box>
58
+ <Box mb="20px">
59
+ <Banner
60
+ {...args}
61
+ type="condensed"
62
+ variant="neutral"
63
+ message="Neutral message."
64
+ />
65
+ </Box>
34
66
  <Box mb="20px">
35
67
  <Banner
36
68
  {...args}
@@ -13,6 +13,7 @@ export interface BannerProps {
13
13
  message: ReactNode;
14
14
  buttonText?: string;
15
15
  onClick?: () => void;
16
+ type?: 'condensed' | 'expanded';
16
17
  }
17
18
 
18
19
  /**
@@ -23,6 +24,7 @@ export const Banner: React.FC<BannerProps> = ({
23
24
  message,
24
25
  buttonText,
25
26
  onClick,
27
+ type = 'expanded',
26
28
  }) => {
27
29
  const Icon = useMemo(() => {
28
30
  switch (variant) {
@@ -42,19 +44,28 @@ export const Banner: React.FC<BannerProps> = ({
42
44
  return (
43
45
  <Alert variant={variant}>
44
46
  <AlertDescription>
45
- <Box pb="8px">{Icon}</Box>
46
- {message}
47
- {onClick && buttonText && (
48
- <Flex pt="8px" justifyContent="flex-end">
49
- <Button
50
- variant="secondary"
51
- onClick={onClick}
52
- text={buttonText}
53
- width="variable"
54
- ariaLabel="banner button"
55
- />
56
- </Flex>
57
- )}
47
+ <Flex
48
+ flexDirection={type === 'condensed' ? 'row' : 'column'}
49
+ alignItems={type === 'condensed' ? 'center' : ''}
50
+ >
51
+ <Box pr="8px">{Icon}</Box>
52
+ <Box pt={type === 'condensed' ? 0 : '8px'}> {message}</Box>
53
+ {onClick && buttonText && (
54
+ <Flex
55
+ ml={type === 'condensed' ? 'auto' : ''}
56
+ pt={type === 'condensed' ? 0 : '8px'}
57
+ justifyContent={type === 'condensed' ? 'flex-end' : 'flex-end'}
58
+ >
59
+ <Button
60
+ variant="secondary"
61
+ onClick={onClick}
62
+ text={buttonText}
63
+ width="variable"
64
+ ariaLabel="banner button"
65
+ />
66
+ </Flex>
67
+ )}
68
+ </Flex>
58
69
  </AlertDescription>
59
70
  </Alert>
60
71
  );
@@ -37,7 +37,7 @@ const meta: Meta<ButtonProps> = {
37
37
  };
38
38
  export default meta;
39
39
  const Template: Story<ButtonProps> = args => (
40
- <Flex flexDir="column" height="200px" justifyContent="space-between">
40
+ <Flex flexDir="column" height="150px" justifyContent="space-between">
41
41
  <Button {...args} text="Primary Fixed" variant="primary" width="fixed" />
42
42
  <Button
43
43
  {...args}
@@ -15,14 +15,13 @@ export const SpinnerButton: React.FC<SpinnerButtonProps> = ({
15
15
  onClick,
16
16
  type,
17
17
  ariaLabel,
18
- variant = 'solid',
19
-
18
+ variant = 'primary',
20
19
  disabled,
21
20
  className,
22
21
  }) => {
23
22
  return (
24
23
  <Button
25
- spinner={<Spinner size={'md'} />}
24
+ spinner={<Spinner size={'sm'} />}
26
25
  isLoading={isLoading}
27
26
  onClick={onClick}
28
27
  type={type}
@@ -5,7 +5,6 @@ import { Input, InputProps } from '.';
5
5
  import { useFormHandler } from '../form/hooks/useFormHandler';
6
6
  import * as Yup from 'yup';
7
7
  import { Form } from '../form';
8
- import { Box } from '@chakra-ui/react';
9
8
 
10
9
  const meta: Meta<InputProps<StoryFormSchema>> = {
11
10
  title: 'Input example',
@@ -1,5 +1,12 @@
1
- import React, { useEffect, useRef, useState } from 'react';
1
+ import React, {
2
+ KeyboardEventHandler,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
2
8
  import { Box, Flex, Text, Image, Input } from '@chakra-ui/react';
9
+ import { debounce } from 'lodash';
3
10
  import {
4
11
  FieldOption,
5
12
  FieldOptions,
@@ -44,7 +51,13 @@ const StackedMultiSelect = React.forwardRef<
44
51
  const [localOptions, setLocalOptions] = useState<FieldOptions>(options);
45
52
  const [isFocussed, setIsFocussed] = useState(false);
46
53
  const [shouldSideScroll, setShouldSideScroll] = useState(false);
54
+ const [optionIndex, setOptionIndex] = useState<number | null>(null);
55
+
47
56
  const [position, setPosition] = useState<'top' | 'bottom'>('top');
57
+ const [searchValue, setSearchValue] = useState('');
58
+ const [debouncedSearchValue, setDebouncedSearchValue] = useState('');
59
+
60
+ console.log({ searchValue, debouncedSearchValue });
48
61
 
49
62
  const boundingClientRect = dropdownRef.current?.getBoundingClientRect() as DOMRect;
50
63
 
@@ -122,27 +135,105 @@ const StackedMultiSelect = React.forwardRef<
122
135
  );
123
136
  };
124
137
 
125
- return (
126
- <Box
127
- ref={dropdownRef}
128
- position="relative"
129
- onKeyDown={e => {
130
- if (isFocussed) {
131
- if (e.key === 'Tab') {
132
- return setIsFocussed(false);
133
- }
138
+ const handleOnKeyDown: KeyboardEventHandler<HTMLInputElement> = e => {
139
+ const initialOptionIndex = options[0].value === 'section_header' ? 1 : 0;
140
+
141
+ if (
142
+ !isFocussed &&
143
+ (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown')
144
+ ) {
145
+ setIsFocussed(true);
146
+ return setOptionIndex(initialOptionIndex);
147
+ }
148
+
149
+ if (isFocussed) {
150
+ if (
151
+ optionIndex === null &&
152
+ (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown')
153
+ ) {
154
+ return setOptionIndex(initialOptionIndex);
155
+ }
156
+
157
+ if (e.key === 'ArrowUp' && optionIndex !== null && optionIndex > 0) {
158
+ const incrementValue =
159
+ localOptions[optionIndex - 1] &&
160
+ localOptions[optionIndex - 1].value === 'section_header'
161
+ ? 2
162
+ : 1;
163
+ setOptionIndex(optionIndex - incrementValue);
164
+
165
+ return dropdownMenuRef.current?.scrollTo({
166
+ top: optionIndex * 24,
167
+ behavior: 'smooth',
168
+ });
169
+ }
170
+
171
+ if (
172
+ e.key === 'ArrowDown' &&
173
+ optionIndex !== null &&
174
+ optionIndex < localOptions.length
175
+ ) {
176
+ const incrementValue =
177
+ localOptions[optionIndex + 1] &&
178
+ localOptions[optionIndex + 1].value === 'section_header'
179
+ ? 2
180
+ : 1;
181
+ setOptionIndex(optionIndex + incrementValue);
182
+
183
+ return dropdownMenuRef.current?.scrollTo({
184
+ top: optionIndex * 24,
185
+ behavior: 'smooth',
186
+ });
187
+ }
188
+
189
+ if (e.key === 'Enter' && optionIndex !== null) {
190
+ const option = localOptions.find((_, idx) => optionIndex === idx);
191
+ if (!option) return;
134
192
 
135
- const idx = options.findIndex(
136
- option => option.label[0].toLocaleLowerCase() === e.key
137
- );
138
-
139
- dropdownMenuRef.current?.scrollTo({
140
- top: idx * 27,
141
- behavior: 'smooth',
142
- });
143
- }
144
- }}
145
- >
193
+ handleChange(option);
194
+
195
+ return setIsFocussed(false);
196
+ }
197
+
198
+ if (e.key === 'Tab') {
199
+ return setIsFocussed(false);
200
+ }
201
+
202
+ return update(debouncedSearchValue.concat(e.key));
203
+ }
204
+ };
205
+
206
+ useEffect(() => {
207
+ if (searchValue.length) {
208
+ const idx = options.findIndex(
209
+ option =>
210
+ option.label.substring(0, searchValue.length).toLowerCase() ===
211
+ searchValue.toLowerCase()
212
+ );
213
+
214
+ dropdownMenuRef.current?.scrollTo({
215
+ top: idx * 24,
216
+ behavior: 'smooth',
217
+ });
218
+
219
+ setSearchValue('');
220
+ setDebouncedSearchValue('');
221
+ }
222
+ }, [options, searchValue]);
223
+
224
+ const updateSearchValue = useMemo(() => {
225
+ return debounce(val => {
226
+ setSearchValue(val);
227
+ }, 1000);
228
+ }, []);
229
+
230
+ const update = (value: string) => {
231
+ updateSearchValue(value);
232
+ setDebouncedSearchValue(value);
233
+ };
234
+
235
+ return (
236
+ <Box ref={dropdownRef} position="relative" onKeyDown={handleOnKeyDown}>
146
237
  <Flex
147
238
  fontSize="13px"
148
239
  h="26px"
@@ -218,6 +309,7 @@ const StackedMultiSelect = React.forwardRef<
218
309
  onSelectItem={option => handleChange(option)}
219
310
  options={localOptions}
220
311
  position={position}
312
+ optionIndex={optionIndex}
221
313
  />
222
314
  )}
223
315
  </Box>
@@ -0,0 +1,236 @@
1
+ import React, {
2
+ KeyboardEventHandler,
3
+ useEffect,
4
+ useRef,
5
+ useMemo,
6
+ useState,
7
+ } from 'react';
8
+ import {
9
+ Box,
10
+ Image,
11
+ Input,
12
+ InputGroup,
13
+ InputRightElement,
14
+ } from '@chakra-ui/react';
15
+ import { FieldOptions } from '../InputTypes';
16
+ import { StackedInputProps } from '../StackedInput/StackedInput';
17
+ import colors from '../../../theme/foundations/colors';
18
+ import { UseFormSetValue, FieldValues, Control } from 'react-hook-form';
19
+ import SubtractIcon from './assets/svg/subtract.svg';
20
+ import { Dropdown } from '../components/dropdown';
21
+ import useDidMountEffect from '../../../hooks/useDidMountEffect';
22
+ import { useOnClickOutside } from '../../../hooks/useOnOutsideClick';
23
+ import { debounce } from 'lodash';
24
+
25
+ export interface StackedSelectProps extends StackedInputProps {
26
+ options: FieldOptions;
27
+ setValue: UseFormSetValue<FieldValues>;
28
+ control: Control<FieldValues, any>;
29
+ handleOnChange: (value?: string) => void;
30
+ }
31
+
32
+ /**
33
+ * A functional React component utilized to render the `StackedSelect` component.
34
+ */
35
+ const StackedSelect = React.forwardRef<HTMLInputElement, StackedSelectProps>(
36
+ (
37
+ {
38
+ isRequired,
39
+ options,
40
+ name,
41
+ setValue,
42
+ handleOnChange,
43
+ disabled,
44
+ value,
45
+ ...props
46
+ },
47
+ _ref
48
+ ) => {
49
+ const dropdownRef = useRef<HTMLDivElement>(null);
50
+ const dropdownMenuRef = useRef<HTMLDivElement>(null);
51
+
52
+ const [isFocussed, setIsFocussed] = useState(false);
53
+ const [selectedOption, setSelectedOption] = useState(
54
+ options.find(option => option.value === value)?.label ?? ''
55
+ );
56
+ const [optionIndex, setOptionIndex] = useState<number | null>(null);
57
+ const [position, setPosition] = useState<'top' | 'bottom'>('top');
58
+ const [searchValue, setSearchValue] = useState('');
59
+ const [debouncedSearchValue, setDebouncedSearchValue] = useState('');
60
+
61
+ const boundingClientRect = dropdownRef.current?.getBoundingClientRect() as DOMRect;
62
+
63
+ useEffect(() => {
64
+ const boundingClientRect = dropdownRef.current?.getBoundingClientRect() as DOMRect;
65
+
66
+ if (window.innerHeight - (boundingClientRect?.y + 240) >= 0) {
67
+ setPosition('top');
68
+ } else {
69
+ setPosition('bottom');
70
+ }
71
+ }, [boundingClientRect]);
72
+
73
+ useDidMountEffect(() => {
74
+ setSelectedOption(
75
+ options.find(option => option.value === value)?.label ?? ''
76
+ );
77
+ }, [value]);
78
+
79
+ useOnClickOutside(dropdownRef, () => setIsFocussed(false));
80
+
81
+ const handleOnSelectItem = (option: {
82
+ label: string;
83
+ value: string;
84
+ sortValue: number;
85
+ }) => {
86
+ if (handleOnChange) {
87
+ handleOnChange(option.value);
88
+ }
89
+ setValue(name as string, option.value);
90
+ setSelectedOption(option.label);
91
+ setIsFocussed(false);
92
+ };
93
+
94
+ const handleOnKeyDown: KeyboardEventHandler<HTMLInputElement> = e => {
95
+ const initialOptionIndex = options[0].value === 'section_header' ? 1 : 0;
96
+
97
+ if (
98
+ !isFocussed &&
99
+ (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown')
100
+ ) {
101
+ setIsFocussed(true);
102
+ return setOptionIndex(initialOptionIndex);
103
+ }
104
+
105
+ if (isFocussed) {
106
+ if (
107
+ optionIndex === null &&
108
+ (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown')
109
+ ) {
110
+ return setOptionIndex(initialOptionIndex);
111
+ }
112
+
113
+ if (e.key === 'ArrowUp' && optionIndex !== null && optionIndex > 0) {
114
+ const incrementValue =
115
+ options[optionIndex - 1] &&
116
+ options[optionIndex - 1].value === 'section_header'
117
+ ? 2
118
+ : 1;
119
+ setOptionIndex(optionIndex - incrementValue);
120
+
121
+ return dropdownMenuRef.current?.scrollTo({
122
+ top: optionIndex * 24,
123
+ behavior: 'smooth',
124
+ });
125
+ }
126
+
127
+ if (
128
+ e.key === 'ArrowDown' &&
129
+ optionIndex !== null &&
130
+ optionIndex < options.length
131
+ ) {
132
+ const incrementValue =
133
+ options[optionIndex + 1] &&
134
+ options[optionIndex + 1].value === 'section_header'
135
+ ? 2
136
+ : 1;
137
+ setOptionIndex(optionIndex + incrementValue);
138
+
139
+ return dropdownMenuRef.current?.scrollTo({
140
+ top: optionIndex * 24,
141
+ behavior: 'smooth',
142
+ });
143
+ }
144
+
145
+ if (e.key === 'Enter' && optionIndex !== null) {
146
+ const option = options.find((_, idx) => optionIndex === idx);
147
+ if (!option) return;
148
+
149
+ if (handleOnChange) {
150
+ handleOnChange(option.value);
151
+ }
152
+
153
+ setSelectedOption(option?.label);
154
+ setValue(name as string, option.value, {
155
+ shouldDirty: true,
156
+ shouldValidate: true,
157
+ });
158
+
159
+ return setIsFocussed(false);
160
+ }
161
+
162
+ if (e.key === 'Tab') {
163
+ return setIsFocussed(false);
164
+ }
165
+ }
166
+ };
167
+
168
+ useEffect(() => {
169
+ if (searchValue.length) {
170
+ const idx = options.findIndex(
171
+ option =>
172
+ option.label.substring(0, searchValue.length).toLowerCase() ===
173
+ searchValue.toLowerCase()
174
+ );
175
+
176
+ dropdownMenuRef.current?.scrollTo({
177
+ top: idx * 24,
178
+ behavior: 'smooth',
179
+ });
180
+
181
+ setSearchValue('');
182
+ setDebouncedSearchValue('');
183
+ }
184
+ }, [options, searchValue]);
185
+
186
+ const updateSearchValue = useMemo(() => {
187
+ return debounce(val => {
188
+ setSearchValue(val);
189
+ }, 1000);
190
+ }, []);
191
+
192
+ const update = (value: string) => {
193
+ updateSearchValue(value);
194
+ setDebouncedSearchValue(value);
195
+ };
196
+
197
+ return (
198
+ <Box ref={dropdownRef} position="relative">
199
+ <InputGroup>
200
+ <Input
201
+ isRequired={isRequired}
202
+ {...props}
203
+ ref={_ref}
204
+ onClick={() => setIsFocussed(!isFocussed)}
205
+ cursor="pointer"
206
+ color="transparent"
207
+ fontSize="13px"
208
+ textShadow={`0 0 0 ${colors.label.primary.light}`}
209
+ value={selectedOption}
210
+ disabled={disabled}
211
+ autoComplete="off"
212
+ onChange={e => update(debouncedSearchValue.concat(e.target.value))}
213
+ onKeyDown={handleOnKeyDown}
214
+ />
215
+ <InputRightElement
216
+ cursor={disabled ? 'not-allowed' : 'pointer'}
217
+ onClick={() => !disabled && setIsFocussed(!isFocussed)}
218
+ >
219
+ <Image src={SubtractIcon} alt="subtract" boxSize="16px" />
220
+ </InputRightElement>
221
+ </InputGroup>
222
+ {isFocussed && (
223
+ <Dropdown
224
+ position={position}
225
+ dropdownRef={dropdownMenuRef}
226
+ onSelectItem={option => handleOnSelectItem(option)}
227
+ options={options}
228
+ optionIndex={optionIndex}
229
+ />
230
+ )}
231
+ </Box>
232
+ );
233
+ }
234
+ );
235
+
236
+ export default StackedSelect;
@@ -8,6 +8,7 @@ export interface DropdownProps {
8
8
  options: FieldOptions;
9
9
  dropdownRef: RefObject<HTMLDivElement>;
10
10
  position: 'top' | 'bottom';
11
+ optionIndex?: number | null;
11
12
  }
12
13
 
13
14
  /**
@@ -18,6 +19,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
18
19
  options,
19
20
  dropdownRef,
20
21
  position,
22
+ optionIndex,
21
23
  }) => {
22
24
  const DropdownContent = useMemo(() => {
23
25
  return options.map((option, idx) => (
@@ -55,22 +57,27 @@ export const Dropdown: React.FC<DropdownProps> = ({
55
57
  px="8px"
56
58
  py="4px"
57
59
  width="100%"
58
- color={colors.label.primary.light}
60
+ color={
61
+ optionIndex === idx
62
+ ? colors.label.primary.dark
63
+ : colors.label.primary.light
64
+ }
59
65
  _hover={{
60
66
  color: colors.label.primary.dark,
61
67
  bg: colors.fill.action,
62
68
  borderRadius: '4px',
63
69
  width: '100%',
64
70
  }}
65
- bg="inherit"
71
+ bg={optionIndex === idx ? colors.fill.action : 'inherit'}
66
72
  whiteSpace="nowrap"
73
+ id={option.value}
67
74
  >
68
75
  {option.label}
69
76
  </Box>
70
77
  )}
71
78
  </>
72
79
  ));
73
- }, [onSelectItem, options]);
80
+ }, [onSelectItem, optionIndex, options]);
74
81
 
75
82
  return (
76
83
  <Flex
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import StackedCheckBox from './StackedCheckbox/StackedCheckbox';
3
3
  import StackedInput from './StackedInput/StackedInput';
4
4
  import StackedRadioGroup from './StackedRadio/StackedRadioGroup';
5
- import StackedSelect from './StackedSelect/StackedSelect';
5
+ import StackedSelect from './StackedSelect';
6
6
  import StackedTextarea from './StackedTextarea/StackedTextarea';
7
7
  import { FieldOptions, ValidationProps, InputType } from './InputTypes';
8
8
  import {
@@ -38,7 +38,7 @@ export const EmptyTable: React.FC = () => {
38
38
  <Tbody>
39
39
  {Array.from({ length: 14 }, (_, i) => i + 1).map(i => (
40
40
  <Tr>
41
- <Td height="23px" opacity={getOpacity(i)}></Td>
41
+ <Td height="26px" opacity={getOpacity(i)}></Td>
42
42
  </Tr>
43
43
  ))}
44
44
  </Tbody>
@@ -6,6 +6,7 @@ const baseStyle = defineStyle({
6
6
  fontSize: '13px',
7
7
  bg: colors.fill.action,
8
8
  color: colors.label.primary.dark,
9
+ h: '26px',
9
10
  border: 'none',
10
11
  px: '8px',
11
12
  py: '4px',
@@ -11,8 +11,12 @@ const baseStyle = {
11
11
  },
12
12
  tr: {
13
13
  fontSize: '13px',
14
+ h: '26px',
15
+ lineHeight: 'normal',
14
16
  _odd: {
15
17
  td: {
18
+ h: '26px ',
19
+ lineHeight: 'normal',
16
20
  bg: colors.fill.light.tertiary,
17
21
  _first: {
18
22
  borderTopLeftRadius: 'md',
@@ -27,6 +31,8 @@ const baseStyle = {
27
31
  },
28
32
  td: {
29
33
  padding: '5px 8px !important',
34
+ lineHeight: 'normal',
35
+ h: '26px',
30
36
  },
31
37
  };
32
38
 
@@ -7,7 +7,7 @@ const baseStyle: Partial<TextProps> = {
7
7
  fontWeight: typography.fontWeights.normal,
8
8
  fontFamily: typography.fonts.base,
9
9
  fontSize: typography.fontSizes.sm,
10
- lineHeight: typography.lineHeights.base,
10
+ lineHeight: typography.lineHeights.normal,
11
11
  letterSpacing: typography.letterSpacings.wide,
12
12
  };
13
13
 
@@ -8,6 +8,7 @@ export default {
8
8
  base: 'visible',
9
9
  lg: 'hidden',
10
10
  },
11
+ lineHeight: 'normal',
11
12
  },
12
13
  '*, *::before, *::after': {
13
14
  borderColor: 'gray.200',