@arbor-education/design-system.components 0.5.1 → 0.5.3
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 +12 -0
- package/dist/components/searchBar/SearchBar.d.ts +2 -0
- package/dist/components/searchBar/SearchBar.d.ts.map +1 -1
- package/dist/components/searchBar/SearchBar.js +27 -8
- package/dist/components/searchBar/SearchBar.js.map +1 -1
- package/dist/components/searchBar/SearchBar.stories.d.ts +46 -0
- package/dist/components/searchBar/SearchBar.stories.d.ts.map +1 -0
- package/dist/components/searchBar/SearchBar.stories.js +77 -0
- package/dist/components/searchBar/SearchBar.stories.js.map +1 -0
- package/dist/components/searchBar/SearchBar.test.js +104 -20
- package/dist/components/searchBar/SearchBar.test.js.map +1 -1
- package/dist/components/table/Table.stories.d.ts +1 -0
- package/dist/components/table/Table.stories.d.ts.map +1 -1
- package/dist/components/table/Table.stories.js +15 -3
- package/dist/components/table/Table.stories.js.map +1 -1
- package/dist/components/table/Table.test.js +27 -0
- package/dist/components/table/Table.test.js.map +1 -1
- package/dist/components/table/cellRenderers/ButtonCellRenderer.d.ts.map +1 -1
- package/dist/components/table/cellRenderers/ButtonCellRenderer.js.map +1 -1
- package/dist/components/table/cellRenderers/InlineTextCellRenderer.d.ts.map +1 -1
- package/dist/components/table/cellRenderers/InlineTextCellRenderer.js +28 -3
- package/dist/components/table/cellRenderers/InlineTextCellRenderer.js.map +1 -1
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.d.ts +2 -1
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.d.ts.map +1 -1
- package/dist/index.css +36 -26
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/searchBar/SearchBar.stories.tsx +86 -0
- package/src/components/searchBar/SearchBar.test.tsx +120 -20
- package/src/components/searchBar/SearchBar.tsx +47 -17
- package/src/components/searchBar/searchBar.scss +69 -43
- package/src/components/table/Table.stories.tsx +16 -3
- package/src/components/table/Table.test.tsx +31 -0
- package/src/components/table/cellRenderers/ButtonCellRenderer.tsx +3 -1
- package/src/components/table/cellRenderers/InlineTextCellRenderer.tsx +41 -4
- package/src/components/table/cellRenderers/SelectDropdownCellRenderer.tsx +1 -1
- package/src/components/table/table.scss +2 -12
- package/src/index.ts +1 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { SearchBar } from './SearchBar';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/SearchBar',
|
|
7
|
+
component: SearchBar,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'padded',
|
|
10
|
+
},
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
argTypes: {
|
|
13
|
+
searchValue: {
|
|
14
|
+
control: 'text',
|
|
15
|
+
description: 'The current search input value',
|
|
16
|
+
},
|
|
17
|
+
setSearchValue: {
|
|
18
|
+
action: 'search value changed',
|
|
19
|
+
description: 'Callback fired when the search input changes',
|
|
20
|
+
},
|
|
21
|
+
placeholderText: {
|
|
22
|
+
control: 'text',
|
|
23
|
+
description: 'Text displayed in the inactive/collapsed state',
|
|
24
|
+
},
|
|
25
|
+
hoverText: {
|
|
26
|
+
control: 'text',
|
|
27
|
+
description: 'Text displayed when hovering the inactive state',
|
|
28
|
+
},
|
|
29
|
+
alwaysOpen: {
|
|
30
|
+
control: 'boolean',
|
|
31
|
+
description: 'Whether the search bar should be always open',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
} satisfies Meta<typeof SearchBar>;
|
|
35
|
+
|
|
36
|
+
export default meta;
|
|
37
|
+
type Story = StoryObj<typeof meta>;
|
|
38
|
+
|
|
39
|
+
const InteractiveSearchBar = (args: React.ComponentProps<typeof SearchBar>) => {
|
|
40
|
+
const [value, setValue] = useState(args.searchValue ?? '');
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setValue(args.searchValue ?? '');
|
|
43
|
+
}, [args.searchValue]);
|
|
44
|
+
return <SearchBar {...args} searchValue={value} setSearchValue={setValue} />;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Default: Story = {
|
|
48
|
+
render: args => <InteractiveSearchBar {...args} />,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const WithPlaceholder: Story = {
|
|
52
|
+
render: args => <InteractiveSearchBar {...args} />,
|
|
53
|
+
args: {
|
|
54
|
+
placeholderText: 'Search...',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const WithHoverText: Story = {
|
|
59
|
+
render: args => <InteractiveSearchBar {...args} />,
|
|
60
|
+
args: {
|
|
61
|
+
placeholderText: 'Search...',
|
|
62
|
+
hoverText: 'Start searching',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const WithHoverTextNoPlaceholder: Story = {
|
|
67
|
+
render: args => <InteractiveSearchBar {...args} />,
|
|
68
|
+
args: {
|
|
69
|
+
hoverText: 'Start searching',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const WithSearchValue: Story = {
|
|
74
|
+
render: args => <InteractiveSearchBar {...args} />,
|
|
75
|
+
args: {
|
|
76
|
+
searchValue: 'Jacob Black',
|
|
77
|
+
placeholderText: 'Search...',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const AlwaysOpen: Story = {
|
|
82
|
+
render: args => <InteractiveSearchBar {...args} />,
|
|
83
|
+
args: {
|
|
84
|
+
alwaysOpen: true,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
@@ -11,30 +11,130 @@ describe('SearchBar component', () => {
|
|
|
11
11
|
setSearchMock.mockClear();
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
describe('inactive state', () => {
|
|
15
|
+
test('renders search bar with placeholder text', () => {
|
|
16
|
+
render(<SearchBar setSearchValue={setSearchMock} placeholderText="Search for something" />);
|
|
17
|
+
expect(screen.getByText('Search for something')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('renders search bar with icon only when no placeholder text', () => {
|
|
21
|
+
render(<SearchBar setSearchValue={setSearchMock} />);
|
|
22
|
+
expect(screen.getByTestId('search-bar-inactive')).toHaveClass('ds-search-bar--inactive-icon-only');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('clicking the search icon opens the search bar', async () => {
|
|
26
|
+
render(<SearchBar setSearchValue={setSearchMock} />);
|
|
27
|
+
await userEvent.click(screen.getByTestId('search-bar-inactive'));
|
|
28
|
+
expect(screen.getByLabelText('Search input')).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('shows hoverText on mouse enter', async () => {
|
|
32
|
+
render(<SearchBar setSearchValue={setSearchMock} placeholderText="Search" hoverText="Type to search" />);
|
|
33
|
+
fireEvent.mouseEnter(screen.getByTestId('search-bar-inactive'));
|
|
34
|
+
expect(screen.getByText('Type to search')).toBeInTheDocument();
|
|
35
|
+
});
|
|
18
36
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
37
|
+
test('restores placeholderText on mouse leave', async () => {
|
|
38
|
+
render(<SearchBar setSearchValue={setSearchMock} placeholderText="Search" hoverText="Type to search" />);
|
|
39
|
+
const button = screen.getByTestId('search-bar-inactive');
|
|
40
|
+
fireEvent.mouseEnter(button);
|
|
41
|
+
fireEvent.mouseLeave(button);
|
|
42
|
+
expect(screen.getByText('Search')).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('keeps placeholder on hover when no hoverText provided', () => {
|
|
46
|
+
render(<SearchBar setSearchValue={setSearchMock} placeholderText="Search" />);
|
|
47
|
+
const button = screen.getByTestId('search-bar-inactive');
|
|
48
|
+
fireEvent.mouseEnter(button);
|
|
49
|
+
expect(screen.getByText('Search')).toBeInTheDocument();
|
|
50
|
+
});
|
|
22
51
|
});
|
|
23
52
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
53
|
+
describe('active state', () => {
|
|
54
|
+
test('shows active search bar immediately when searchValue is provided', () => {
|
|
55
|
+
render(<SearchBar setSearchValue={setSearchMock} searchValue="hello" />);
|
|
56
|
+
expect(screen.getByLabelText('Search input')).toBeInTheDocument();
|
|
57
|
+
expect(screen.queryByTestId('search-bar-inactive')).not.toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('shows active search bar immediately when alwaysOpen is true', () => {
|
|
61
|
+
render(<SearchBar setSearchValue={setSearchMock} alwaysOpen={true} />);
|
|
62
|
+
expect(screen.getByLabelText('Search input')).toBeInTheDocument();
|
|
63
|
+
expect(screen.queryByTestId('search-bar-inactive')).not.toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('calls setSearchValue when input changes', async () => {
|
|
67
|
+
render(<SearchBar setSearchValue={setSearchMock} />);
|
|
68
|
+
await userEvent.click(screen.getByTestId('search-bar-inactive'));
|
|
69
|
+
const input = screen.getByLabelText('Search input');
|
|
70
|
+
fireEvent.change(input, { target: { value: 'Hello' } });
|
|
71
|
+
expect(setSearchMock).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(setSearchMock).toHaveBeenCalledWith('Hello');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('does not throw when setSearchValue is not provided and user types', async () => {
|
|
76
|
+
render(<SearchBar alwaysOpen={true} />);
|
|
77
|
+
const input = screen.getByLabelText('Search input');
|
|
78
|
+
expect(() => fireEvent.change(input, { target: { value: 'Hello' } })).not.toThrow();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('renders clear button when active and alwaysOpen is false', async () => {
|
|
82
|
+
render(<SearchBar setSearchValue={setSearchMock} alwaysOpen={false} />);
|
|
83
|
+
await userEvent.click(screen.getByTestId('search-bar-inactive'));
|
|
84
|
+
expect(screen.getByRole('button', { name: 'Clear search' })).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('does not render clear button when alwaysOpen is true', () => {
|
|
88
|
+
render(<SearchBar setSearchValue={setSearchMock} alwaysOpen={true} />);
|
|
89
|
+
expect(screen.queryByRole('button', { name: 'Clear search' })).not.toBeInTheDocument();
|
|
90
|
+
});
|
|
31
91
|
});
|
|
32
92
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
93
|
+
describe('clear behaviour', () => {
|
|
94
|
+
test('clicking clear calls setSearchValue with empty string', async () => {
|
|
95
|
+
render(<SearchBar setSearchValue={setSearchMock} />);
|
|
96
|
+
await userEvent.click(screen.getByTestId('search-bar-inactive'));
|
|
97
|
+
await userEvent.click(screen.getByRole('button', { name: 'Clear search' }));
|
|
98
|
+
expect(setSearchMock).toHaveBeenCalledWith('');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('clicking clear hides the search bar and shows inactive state', async () => {
|
|
102
|
+
render(<SearchBar setSearchValue={setSearchMock} />);
|
|
103
|
+
await userEvent.click(screen.getByTestId('search-bar-inactive'));
|
|
104
|
+
await userEvent.click(screen.getByRole('button', { name: 'Clear search' }));
|
|
105
|
+
expect(screen.getByTestId('search-bar-inactive')).toBeInTheDocument();
|
|
106
|
+
expect(screen.queryByLabelText('Search input')).not.toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('does not throw when setSearchValue is not provided and clear is pressed', async () => {
|
|
110
|
+
render(<SearchBar searchValue="hello" />);
|
|
111
|
+
expect(() =>
|
|
112
|
+
fireEvent.click(screen.getByRole('button', { name: 'Clear search' })),
|
|
113
|
+
).not.toThrow();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('pressing Enter on clear button triggers clear', async () => {
|
|
117
|
+
render(<SearchBar setSearchValue={setSearchMock} />);
|
|
118
|
+
await userEvent.click(screen.getByTestId('search-bar-inactive'));
|
|
119
|
+
const clearButton = screen.getByRole('button', { name: 'Clear search' });
|
|
120
|
+
fireEvent.keyDown(clearButton, { key: 'Enter' });
|
|
121
|
+
expect(setSearchMock).toHaveBeenCalledWith('');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('pressing Space on clear button triggers clear', async () => {
|
|
125
|
+
render(<SearchBar setSearchValue={setSearchMock} />);
|
|
126
|
+
await userEvent.click(screen.getByTestId('search-bar-inactive'));
|
|
127
|
+
const clearButton = screen.getByRole('button', { name: 'Clear search' });
|
|
128
|
+
fireEvent.keyDown(clearButton, { key: ' ' });
|
|
129
|
+
expect(setSearchMock).toHaveBeenCalledWith('');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('pressing other keys on clear button does not trigger clear', async () => {
|
|
133
|
+
render(<SearchBar setSearchValue={setSearchMock} />);
|
|
134
|
+
await userEvent.click(screen.getByTestId('search-bar-inactive'));
|
|
135
|
+
const clearButton = screen.getByRole('button', { name: 'Clear search' });
|
|
136
|
+
fireEvent.keyDown(clearButton, { key: 'Tab' });
|
|
137
|
+
expect(setSearchMock).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
39
139
|
});
|
|
40
140
|
});
|
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
2
|
import { Button } from 'Components/button/Button';
|
|
3
3
|
import { Icon } from 'Components/icon/Icon';
|
|
4
|
-
import { useRef, useState } from 'react';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
5
|
|
|
6
6
|
type SearchBarProps = {
|
|
7
7
|
searchValue?: string;
|
|
8
8
|
setSearchValue?: (searchValue: string) => void;
|
|
9
9
|
placeholderText?: string;
|
|
10
|
+
hoverText?: string;
|
|
11
|
+
alwaysOpen?: boolean;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
export const SearchBar = (props: SearchBarProps) => {
|
|
13
|
-
const { searchValue, setSearchValue, placeholderText } = props;
|
|
15
|
+
const { searchValue, setSearchValue, placeholderText, hoverText, alwaysOpen = false } = props;
|
|
14
16
|
|
|
15
|
-
const [isSearchVisible, setIsSearchVisible] = useState(!!searchValue);
|
|
17
|
+
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(alwaysOpen || !!searchValue);
|
|
16
18
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
17
19
|
|
|
20
|
+
const [displayedText, setDisplayedText] = useState<string | null>(placeholderText ?? null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setIsSearchVisible(alwaysOpen || !!searchValue);
|
|
24
|
+
}, [alwaysOpen, searchValue]);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
setDisplayedText(placeholderText ?? null);
|
|
28
|
+
}, [placeholderText]);
|
|
29
|
+
|
|
18
30
|
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
19
31
|
setSearchValue?.(event.target.value);
|
|
20
32
|
};
|
|
@@ -38,17 +50,33 @@ export const SearchBar = (props: SearchBarProps) => {
|
|
|
38
50
|
}
|
|
39
51
|
};
|
|
40
52
|
|
|
53
|
+
const handleMouseEnter = () => {
|
|
54
|
+
if (hoverText) {
|
|
55
|
+
setDisplayedText(hoverText);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleMouseLeave = () => {
|
|
60
|
+
if (placeholderText) {
|
|
61
|
+
setDisplayedText(placeholderText);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
setDisplayedText(null);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
41
68
|
if (!isSearchVisible) {
|
|
42
69
|
return (
|
|
43
70
|
<Button
|
|
44
71
|
data-testid="search-bar-inactive"
|
|
45
72
|
className={
|
|
46
73
|
classNames('ds-search-bar--inactive', {
|
|
47
|
-
'ds-search-bar--inactive-icon-only': !
|
|
48
|
-
'ds-search-bar--inactive-with-placeholder': placeholderText,
|
|
74
|
+
'ds-search-bar--inactive-icon-only': !displayedText,
|
|
49
75
|
})
|
|
50
76
|
}
|
|
51
77
|
onClick={handleSearchIconClick}
|
|
78
|
+
onMouseEnter={handleMouseEnter}
|
|
79
|
+
onMouseLeave={handleMouseLeave}
|
|
52
80
|
>
|
|
53
81
|
<Icon
|
|
54
82
|
name="search"
|
|
@@ -56,10 +84,10 @@ export const SearchBar = (props: SearchBarProps) => {
|
|
|
56
84
|
color="var(--search-global-default-color-icon)"
|
|
57
85
|
className="ds-search-bar__icon"
|
|
58
86
|
/>
|
|
59
|
-
{
|
|
87
|
+
{displayedText
|
|
60
88
|
&& (
|
|
61
89
|
<span className="ds-search-bar--inactive__placeholder">
|
|
62
|
-
{
|
|
90
|
+
{displayedText}
|
|
63
91
|
</span>
|
|
64
92
|
)}
|
|
65
93
|
</Button>
|
|
@@ -82,16 +110,18 @@ export const SearchBar = (props: SearchBarProps) => {
|
|
|
82
110
|
ref={searchInputRef}
|
|
83
111
|
aria-label="Search input"
|
|
84
112
|
/>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
113
|
+
{!alwaysOpen && (
|
|
114
|
+
<span
|
|
115
|
+
className="ds-search-bar__clear-container"
|
|
116
|
+
role="button"
|
|
117
|
+
tabIndex={0}
|
|
118
|
+
onClick={handleClear}
|
|
119
|
+
onKeyDown={handleClearKeyDown}
|
|
120
|
+
aria-label="Clear search"
|
|
121
|
+
>
|
|
122
|
+
<Icon name="x" size={12} className="ds-search-bar__clear" />
|
|
123
|
+
</span>
|
|
124
|
+
)}
|
|
95
125
|
</span>
|
|
96
126
|
);
|
|
97
127
|
};
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
.ds-search-bar__icon {
|
|
2
|
-
color: var(--search-global-default-color-icon);
|
|
3
|
-
}
|
|
4
|
-
|
|
5
1
|
.ds-search-bar {
|
|
6
2
|
display: flex;
|
|
7
3
|
align-items: center;
|
|
@@ -9,58 +5,88 @@
|
|
|
9
5
|
border-radius: var(--search-global-radius);
|
|
10
6
|
background: var(--search-global-default-color-background);
|
|
11
7
|
padding: var(--search-global-spacing-vertical) var(--search-global-spacing-horizontal);
|
|
8
|
+
min-height: var(--size-medium);
|
|
9
|
+
width: fit-content;
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
box-shadow: 0 0 0 3px var(--color-brand-500);
|
|
16
|
-
|
|
17
|
-
.ds-search-bar__icon {
|
|
18
|
-
color: var(--search-global-focus-color-icon);
|
|
19
|
-
}
|
|
11
|
+
&__icon {
|
|
12
|
+
color: var(--search-global-default-color-icon);
|
|
20
13
|
}
|
|
21
|
-
}
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
15
|
+
&__clear {
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
display: flex;
|
|
29
20
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
border: none;
|
|
21
|
+
&-container {
|
|
22
|
+
border-radius: var(--border-radius-round);
|
|
23
|
+
padding: var(--spacing-xsmall);
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
}
|
|
25
|
+
&:hover {
|
|
26
|
+
background-color: var(--color-grey-600);
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
height: fit-content;
|
|
45
|
-
align-items: center;
|
|
46
|
-
justify-content: center;
|
|
47
|
-
border: none;
|
|
48
|
-
padding: var(--search-global-spacing-vertical) var(--search-global-spacing-horizontal);
|
|
49
|
-
gap: var(--search-global-spacing-horizontal-gap);
|
|
28
|
+
svg {
|
|
29
|
+
color: var(--color-mono-white);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
50
32
|
|
|
51
|
-
|
|
52
|
-
|
|
33
|
+
&:focus {
|
|
34
|
+
box-shadow: 0 0 0 3px var(--color-brand-300);
|
|
35
|
+
outline: none;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
53
38
|
}
|
|
54
39
|
|
|
55
|
-
|
|
56
|
-
|
|
40
|
+
&__input {
|
|
41
|
+
color: var(--search-global-default-color-text);
|
|
42
|
+
background-color: var(--search-global-default-color-background);
|
|
43
|
+
border: none;
|
|
44
|
+
|
|
45
|
+
&:focus {
|
|
46
|
+
outline: none;
|
|
47
|
+
background: var(--search-global-focus-color-background);
|
|
48
|
+
}
|
|
57
49
|
}
|
|
58
50
|
|
|
59
|
-
&:
|
|
60
|
-
background: var(--search-global-
|
|
51
|
+
&:focus-within {
|
|
52
|
+
background: var(--search-global-focus-color-background);
|
|
53
|
+
box-shadow: 0 0 0 3px var(--color-brand-300);
|
|
61
54
|
|
|
62
55
|
.ds-search-bar__icon {
|
|
63
|
-
color: var(--search-global-
|
|
56
|
+
color: var(--search-global-focus-color-icon);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&--inactive {
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
justify-content: flex-start;
|
|
64
|
+
border: none;
|
|
65
|
+
cursor: text;
|
|
66
|
+
background: var(--search-global-default-color-background);
|
|
67
|
+
padding: var(--search-global-spacing-vertical) var(--search-global-spacing-horizontal);
|
|
68
|
+
|
|
69
|
+
&__placeholder {
|
|
70
|
+
color: var(--search-global-default-color-text);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
&-icon-only {
|
|
74
|
+
padding: var(--search-global-spacing-vertical);
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
height: fit-content;
|
|
77
|
+
width: fit-content;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
&:hover {
|
|
81
|
+
background: var(--search-global-hover-color-background);
|
|
82
|
+
|
|
83
|
+
.ds-search-bar__icon {
|
|
84
|
+
color: var(--search-global-hover-color-icon);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&:focus {
|
|
89
|
+
background: var(--search-global-focus-color-background);
|
|
64
90
|
}
|
|
65
91
|
}
|
|
66
92
|
}
|
|
@@ -578,6 +578,20 @@ export const WithButtonCellRendererThatOpensAModalWithComposableModal: StoryObj<
|
|
|
578
578
|
},
|
|
579
579
|
};
|
|
580
580
|
|
|
581
|
+
export const WithInlineTextCellRenderer: Story = {
|
|
582
|
+
args: {
|
|
583
|
+
rowData: sampleData,
|
|
584
|
+
columnDefs: [{ field: 'name', cellRenderer: 'dsInlineTextCellRenderer', cellRendererParams: {
|
|
585
|
+
supressCellFocusAndFocusFirstElement: true,
|
|
586
|
+
suppressCount: true,
|
|
587
|
+
suppressDoubleClickExpand: true,
|
|
588
|
+
suppressEnterExpand: true,
|
|
589
|
+
} }],
|
|
590
|
+
defaultColDef,
|
|
591
|
+
domLayout: 'autoHeight',
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
|
|
581
595
|
export const WithTreeData: Story = {
|
|
582
596
|
args: {
|
|
583
597
|
columnDefs: [
|
|
@@ -803,11 +817,10 @@ export const TidyTable: Story = {
|
|
|
803
817
|
field: 'name',
|
|
804
818
|
headerName: 'Marksheet Columns',
|
|
805
819
|
headerTooltip: 'Marksheet column or group name',
|
|
806
|
-
editable:
|
|
807
|
-
singleClickEdit: true,
|
|
820
|
+
editable: false,
|
|
808
821
|
cellRenderer: 'agGroupCellRenderer',
|
|
809
|
-
cellEditor: 'agTextCellEditor',
|
|
810
822
|
cellRendererParams: {
|
|
823
|
+
supressCellFocusAndFocusFirstElement: true,
|
|
811
824
|
suppressCount: true,
|
|
812
825
|
suppressDoubleClickExpand: true,
|
|
813
826
|
suppressEnterExpand: true,
|
|
@@ -1303,6 +1303,37 @@ describe('Table', () => {
|
|
|
1303
1303
|
});
|
|
1304
1304
|
});
|
|
1305
1305
|
|
|
1306
|
+
describe('InlineTextCellRenderer', () => {
|
|
1307
|
+
const columnDefs = [{
|
|
1308
|
+
field: 'name',
|
|
1309
|
+
headerName: 'Name',
|
|
1310
|
+
cellRenderer: 'dsInlineTextCellRenderer',
|
|
1311
|
+
}];
|
|
1312
|
+
|
|
1313
|
+
test('renders text input with initial value', async () => {
|
|
1314
|
+
const rowData = [{ name: 'Alice' }];
|
|
1315
|
+
render(<Table columnDefs={columnDefs} rowData={rowData} />);
|
|
1316
|
+
await waitFor(() => expect(screen.getByRole('textbox')).toBeInTheDocument());
|
|
1317
|
+
expect(screen.getByRole('textbox')).toHaveValue('Alice');
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
test('renders text input with empty value when field is empty', async () => {
|
|
1321
|
+
const rowData = [{ name: '' }];
|
|
1322
|
+
render(<Table columnDefs={columnDefs} rowData={rowData} />);
|
|
1323
|
+
await waitFor(() => expect(screen.getByRole('textbox')).toBeInTheDocument());
|
|
1324
|
+
expect(screen.getByRole('textbox')).toHaveValue('');
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
test('updates input value when user types', async () => {
|
|
1328
|
+
const rowData = [{ name: 'Alice' }];
|
|
1329
|
+
render(<Table columnDefs={columnDefs} rowData={rowData} />);
|
|
1330
|
+
const input = await screen.findByRole('textbox');
|
|
1331
|
+
await userEvent.clear(input);
|
|
1332
|
+
await userEvent.type(input, 'Bob');
|
|
1333
|
+
expect(input).toHaveValue('Bob');
|
|
1334
|
+
});
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1306
1337
|
describe('Cell Editing', () => {
|
|
1307
1338
|
describe('with literal data values', () => {
|
|
1308
1339
|
test('supports editing text fields', async () => {
|
|
@@ -2,7 +2,9 @@ import type { CustomCellRendererProps } from 'ag-grid-react';
|
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import { Button, type ButtonProps } from 'Components/button/Button';
|
|
4
4
|
|
|
5
|
-
type ButtonCellRendererProps = CustomCellRendererProps & {
|
|
5
|
+
type ButtonCellRendererProps = CustomCellRendererProps & {
|
|
6
|
+
value: ButtonProps;
|
|
7
|
+
};
|
|
6
8
|
|
|
7
9
|
export const ButtonCellRenderer = (props: ButtonCellRendererProps) => {
|
|
8
10
|
const { value, valueFormatted } = props;
|
|
@@ -1,10 +1,47 @@
|
|
|
1
1
|
import type { CustomCellRendererProps } from 'ag-grid-react';
|
|
2
|
+
import { TextInput } from 'Components/formField/inputs/text/TextInput';
|
|
3
|
+
import { type ChangeEvent, useRef, useState } from 'react';
|
|
4
|
+
import { useComponentDidMount } from 'Utils/hooks/useComponentDidMount';
|
|
2
5
|
|
|
3
|
-
type InlineTextCellRendererProps = CustomCellRendererProps & {
|
|
6
|
+
type InlineTextCellRendererProps = CustomCellRendererProps & {
|
|
7
|
+
value: string;
|
|
8
|
+
};
|
|
4
9
|
|
|
5
10
|
export const InlineTextCellRenderer = (props: InlineTextCellRendererProps) => {
|
|
6
|
-
const { value, valueFormatted } = props;
|
|
7
|
-
const valueToRender = String(valueFormatted ?? value ?? '');
|
|
11
|
+
const { value, valueFormatted, column, node } = props;
|
|
12
|
+
const [valueToRender, setValueToRender] = useState(String(valueFormatted ?? value ?? ''));
|
|
13
|
+
const valueRef = useRef(valueToRender);
|
|
14
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
15
|
+
|
|
16
|
+
const handleChange = (newValue: ChangeEvent<HTMLInputElement>) => {
|
|
17
|
+
const next = typeof newValue === 'string' ? newValue : newValue.target.value;
|
|
18
|
+
setValueToRender(next);
|
|
19
|
+
valueRef.current = next;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
useComponentDidMount(() => {
|
|
23
|
+
const wrapper = wrapperRef.current;
|
|
24
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
25
|
+
if (e.key === 'Enter') {
|
|
26
|
+
e.stopPropagation();
|
|
27
|
+
if (column) {
|
|
28
|
+
node.setDataValue(column, valueRef.current);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
wrapper?.addEventListener('keydown', handleKeyDown);
|
|
34
|
+
return () => {
|
|
35
|
+
wrapper?.removeEventListener('keydown', handleKeyDown);
|
|
36
|
+
};
|
|
37
|
+
});
|
|
8
38
|
|
|
9
|
-
return
|
|
39
|
+
return (
|
|
40
|
+
<div ref={wrapperRef}>
|
|
41
|
+
<TextInput
|
|
42
|
+
value={valueToRender}
|
|
43
|
+
onChange={handleChange}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
10
47
|
};
|
|
@@ -5,7 +5,7 @@ import type { SelectDropdownItemProps } from 'Components/formField/inputs/select
|
|
|
5
5
|
import { SelectDropdown } from 'Components/formField/inputs/selectDropdown/SelectDropdown';
|
|
6
6
|
import { useComponentDidMount } from 'Utils/hooks/useComponentDidMount';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
type SelectDropdownCellRendererProps = CustomCellRendererProps & {
|
|
9
9
|
options?: SelectDropdownItemProps[];
|
|
10
10
|
placeholder?: string;
|
|
11
11
|
};
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
.ds-table {
|
|
2
|
-
&
|
|
3
|
-
|
|
4
|
-
border-radius: var(--form-field-radius);
|
|
5
|
-
border: var(--border-weight) solid var(--form-field-text-default-color-border);
|
|
6
|
-
background: var(--form-field-text-default-color-background);
|
|
7
|
-
color: var(--form-field-text-default-color-text);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
&__select-dropdown {
|
|
11
|
-
.ds-button--dropdown {
|
|
12
|
-
width: 100%;
|
|
13
|
-
}
|
|
2
|
+
&__select-dropdown-cell {
|
|
3
|
+
width: 100%;
|
|
14
4
|
}
|
|
15
5
|
|
|
16
6
|
&__container {
|
package/src/index.ts
CHANGED
|
@@ -33,5 +33,6 @@ export { Toast } from 'Components/toast/Toast';
|
|
|
33
33
|
export { DatePicker } from 'Components/datePicker/DatePicker';
|
|
34
34
|
export { Avatar } from 'Components/avatar/Avatar';
|
|
35
35
|
export { UserDropdown } from 'Components/userDropdown/UserDropdown';
|
|
36
|
+
export { SearchBar } from 'Components/searchBar/SearchBar';
|
|
36
37
|
export type { UserDropdownUserInfoAction } from 'Components/userDropdown/UserDropdown';
|
|
37
38
|
export { ArborLogo, GovhubLogo, KeyLogo, SampeopleLogo, RobinLogo, TimetablerLogo } from 'Components/userDropdown/assets/logos';
|