@douglasneuroinformatics/libui 3.1.2 → 3.1.4

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@douglasneuroinformatics/libui",
3
3
  "type": "module",
4
- "version": "3.1.2",
4
+ "version": "3.1.4",
5
5
  "packageManager": "pnpm@9.3.0",
6
6
  "description": "Generic UI components for DNP projects, built using React and Tailwind CSS",
7
7
  "author": "Joshua Unrau",
@@ -119,6 +119,7 @@
119
119
  "@douglasneuroinformatics/prettier-config": "^0.0.1",
120
120
  "@douglasneuroinformatics/semantic-release": "^0.0.1",
121
121
  "@douglasneuroinformatics/tsconfig": "^1.0.2",
122
+ "@faker-js/faker": "^9.0.1",
122
123
  "@storybook/addon-essentials": "^8.2.9",
123
124
  "@storybook/addon-interactions": "^8.2.9",
124
125
  "@storybook/addon-links": "^8.2.9",
@@ -5,8 +5,8 @@ import { Accordion } from './Accordion';
5
5
 
6
6
  const TEST_ID = 'accordion';
7
7
 
8
- const TestAccordion = () => (
9
- <Accordion collapsible type="single">
8
+ const TestAccordion: React.FC<{ [key: string]: any }> = (props) => (
9
+ <Accordion collapsible type="single" {...props}>
10
10
  <Accordion.Item value="item-1">
11
11
  <Accordion.Trigger>T1</Accordion.Trigger>
12
12
  <Accordion.Content>C1</Accordion.Content>
@@ -19,16 +19,17 @@ describe('Accordion', () => {
19
19
  render(<TestAccordion />);
20
20
  expect(screen.getByTestId(TEST_ID)).toBeDefined();
21
21
  });
22
+ it('should include custom data attributes', () => {
23
+ render(<TestAccordion data-foo="bar" />);
24
+ expect(screen.getByTestId(TEST_ID)).toHaveAttribute('data-foo', 'bar');
25
+ });
22
26
  it('should open and close an item', () => {
23
27
  render(<TestAccordion />);
24
-
25
28
  const toggle = screen.getByText('T1');
26
29
  expect(toggle).toBeInTheDocument();
27
30
  expect(() => screen.getByText('C1')).toThrow();
28
-
29
31
  fireEvent.click(toggle);
30
32
  expect(screen.getByText('C1')).toBeInTheDocument();
31
-
32
33
  fireEvent.click(toggle);
33
34
  expect(() => screen.getByText('C1')).toThrow();
34
35
  });
@@ -0,0 +1,35 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { ActionDropdown } from './ActionDropdown';
5
+
6
+ type Props = React.ComponentPropsWithoutRef<typeof ActionDropdown>;
7
+
8
+ const TEST_ID = 'ActionDropdown';
9
+
10
+ const TestActionDropdown: React.FC<Partial<Props>> = (props) => {
11
+ return (
12
+ <ActionDropdown
13
+ data-testid={TEST_ID}
14
+ options={['Option 1', 'Option 2']}
15
+ title="Action Dropdown"
16
+ onSelection={vi.fn()}
17
+ {...props}
18
+ />
19
+ );
20
+ };
21
+
22
+ describe('ActionDropdown', () => {
23
+ it('should render', () => {
24
+ render(<TestActionDropdown />);
25
+ expect(screen.getByTestId(TEST_ID)).toBeInTheDocument();
26
+ });
27
+ it('should include custom data attributes', () => {
28
+ render(<TestActionDropdown data-foo="bar" />);
29
+ expect(screen.getByTestId(TEST_ID)).toHaveAttribute('data-foo', 'bar');
30
+ });
31
+ it('should contain a custom class name', () => {
32
+ render(<TestActionDropdown className="foo" />);
33
+ expect(screen.getByTestId(TEST_ID)).toHaveClass('foo');
34
+ });
35
+ });
@@ -1,3 +1,5 @@
1
+ import { cn } from '@/utils';
2
+
1
3
  import { DropdownButton } from '../DropdownButton';
2
4
  import { DropdownMenu } from '../DropdownMenu';
3
5
 
@@ -12,8 +14,12 @@ type ActionDropdownOptionKey<T> = T extends readonly string[]
12
14
  : never;
13
15
 
14
16
  export type ActionDropdownProps<T extends ActionDropdownOptions> = {
17
+ [key: `data-${string}`]: unknown;
18
+
15
19
  align?: DropdownMenuContentProps['align'];
16
20
 
21
+ className?: string;
22
+
17
23
  contentClassName?: string;
18
24
 
19
25
  disabled?: boolean;
@@ -35,18 +41,20 @@ export type ActionDropdownProps<T extends ActionDropdownOptions> = {
35
41
  // eslint-disable-next-line react/function-component-definition
36
42
  export function ActionDropdown<const T extends ActionDropdownOptions>({
37
43
  align = 'start',
44
+ className,
38
45
  contentClassName,
39
46
  disabled,
40
47
  onSelection,
41
48
  options,
42
49
  title,
43
50
  triggerClassName,
44
- widthFull
51
+ widthFull,
52
+ ...props
45
53
  }: ActionDropdownProps<T>) {
46
54
  const optionKeys: readonly string[] = options instanceof Array ? options : Object.keys(options);
47
55
  return (
48
56
  <DropdownMenu>
49
- <div className="w-full">
57
+ <div className={cn('w-full', className)} {...props}>
50
58
  <DropdownMenu.Trigger asChild>
51
59
  <DropdownButton className={triggerClassName} disabled={disabled}>
52
60
  {title}
@@ -53,6 +53,7 @@ export type ClientTableColumnProps<T extends ClientTableEntry> = {
53
53
  };
54
54
 
55
55
  export type ClientTableProps<T extends ClientTableEntry> = {
56
+ [key: `data-${string}`]: unknown;
56
57
  className?: string;
57
58
  columnDropdownOptions?: ClientTableDropdownOptions<T>;
58
59
  columns: ClientTableColumn<T>[];
@@ -71,7 +72,8 @@ export const ClientTable = <T extends ClientTableEntry>({
71
72
  entriesPerPage = 10,
72
73
  minRows,
73
74
  noWrap,
74
- onEntryClick
75
+ onEntryClick,
76
+ ...props
75
77
  }: ClientTableProps<T>) => {
76
78
  const [currentPage, setCurrentPage] = useState(1);
77
79
 
@@ -83,7 +85,7 @@ export const ClientTable = <T extends ClientTableEntry>({
83
85
  const nRows = Math.max(currentEntries.length, minRows ?? -1);
84
86
 
85
87
  return (
86
- <div className={className}>
88
+ <div className={className} {...props}>
87
89
  <div className="rounded-md border bg-card tracking-tight text-muted-foreground shadow-sm">
88
90
  <Table>
89
91
  <Table.Header>
@@ -43,7 +43,6 @@ export const DateField = ({ disabled, error, label, name, readOnly, setValue, va
43
43
  <Popover.Trigger>
44
44
  <Input
45
45
  autoComplete="off"
46
- data-cy="date-input"
47
46
  data-testid="date-input"
48
47
  disabled={disabled || readOnly}
49
48
  name={name}
@@ -167,19 +167,12 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
167
167
  )}
168
168
  <div className="flex w-full gap-3">
169
169
  {/** Note - aria-label is used for testing in downstream packages */}
170
- <Button
171
- aria-label="Submit Button"
172
- className="block w-full"
173
- data-cy="submit-form"
174
- disabled={readOnly}
175
- type="submit"
176
- variant="primary"
177
- >
170
+ <Button aria-label="Submit" className="block w-full" disabled={readOnly} type="submit" variant="primary">
178
171
  {submitBtnLabel ?? t('form.submit')}
179
172
  </Button>
180
173
  {resetBtn && (
181
174
  <Button
182
- aria-label="Reset Button"
175
+ aria-label="Reset"
183
176
  className="block w-full"
184
177
  disabled={readOnly}
185
178
  type="button"
@@ -30,19 +30,15 @@ export const NumberFieldSelect = <T extends number = number>({
30
30
  <FieldGroup.Description description={description} />
31
31
  </FieldGroup.Row>
32
32
  <Select name={name} value={value?.toString() ?? ''} onValueChange={(value) => setValue(parseFloat(value) as T)}>
33
- <Select.Trigger
34
- data-cy={`${name}-select-trigger`}
35
- data-testid={`${name}-select-trigger`}
36
- disabled={disabled || readOnly}
37
- >
33
+ <Select.Trigger data-testid={`${name}-select-trigger`} disabled={disabled || readOnly}>
38
34
  <Select.Value />
39
35
  </Select.Trigger>
40
- <Select.Content data-cy={`${name}-select-content`} data-testid={`${name}-select-content`}>
36
+ <Select.Content data-testid={`${name}-select-content`}>
41
37
  {Object.keys(options).map((option) => {
42
38
  // Option needs to be type number (this was a design flaw), but is actually always coerced to string anyways
43
39
  const text = (disableAutoPrefix ? '' : `${option} - `) + options[option as any as T];
44
40
  return (
45
- <Select.Item data-cy={`${name}-select-item-${option}`} key={option} value={option}>
41
+ <Select.Item data-testid={`${name}-select-item-${option}`} key={option} value={option}>
46
42
  {text}
47
43
  </Select.Item>
48
44
  );
@@ -29,16 +29,12 @@ export const StringFieldSelect = <T extends string = string>({
29
29
  <FieldGroup.Description description={description} />
30
30
  </FieldGroup.Row>
31
31
  <Select name={name} value={value ?? ''} onValueChange={(value: T) => setValue(value)}>
32
- <Select.Trigger
33
- data-cy={`${name}-select-trigger`}
34
- data-testid={`${name}-select-trigger`}
35
- disabled={disabled || readOnly}
36
- >
32
+ <Select.Trigger data-testid={`${name}-select-trigger`} disabled={disabled || readOnly}>
37
33
  <Select.Value />
38
34
  </Select.Trigger>
39
- <Select.Content data-cy={`${name}-select-content`} data-testid={`${name}-select-content`}>
35
+ <Select.Content data-testid={`${name}-select-content`}>
40
36
  {Object.keys(options).map((option) => (
41
- <Select.Item data-cy={`${name}-select-item-${option}`} key={option} value={option}>
37
+ <Select.Item data-testid={`${name}-select-item-${option}`} key={option} value={option}>
42
38
  {options[option as T]}
43
39
  </Select.Item>
44
40
  ))}
@@ -47,7 +47,7 @@ export const ListboxDropdown = <T extends ListboxDropdownOption>({
47
47
  <DropdownMenu.CheckboxItem
48
48
  checked={checked}
49
49
  className="flex w-full items-center whitespace-nowrap bg-slate-50 p-2 text-sm hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
50
- data-cy="select-dropdown-option"
50
+ data-testid="select-dropdown-option"
51
51
  key={option.key}
52
52
  onSelect={(event) => {
53
53
  event.preventDefault();
@@ -1,3 +1,4 @@
1
+ import { faker } from '@faker-js/faker';
1
2
  import { vi } from 'vitest';
2
3
 
3
4
  /**
@@ -51,3 +52,11 @@ export const mockStorage = (name: 'localStorage' | 'sessionStorage'): void => {
51
52
  value: new StorageMock()
52
53
  });
53
54
  };
55
+
56
+ export const mockTranslationStore = () => {
57
+ vi.mock('@/hooks/useTranslation', () => ({
58
+ useTranslation: () => ({
59
+ t: () => faker.word.sample()
60
+ })
61
+ }));
62
+ };
@@ -1,9 +1,12 @@
1
1
  import { cleanup } from '@testing-library/react';
2
2
  import { afterEach, vi } from 'vitest';
3
3
 
4
+ import { mockTranslationStore } from './mocks';
5
+
4
6
  import '@testing-library/jest-dom/vitest';
5
7
 
6
8
  vi.mock('zustand');
9
+ mockTranslationStore();
7
10
 
8
11
  // Since we're not using vitest globals, we need to explicitly call cleanup()
9
12
  // for testing-library. See: