@performant-software/semantic-components 1.0.19-beta.0 → 1.0.19-beta.2

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/build/main.css CHANGED
@@ -204,6 +204,11 @@
204
204
  margin-left: 5px;
205
205
  }
206
206
 
207
+ .ui.breadcrumb:first-child {
208
+ margin-bottom: 1em;
209
+ margin-top: 0.5em;
210
+ }
211
+
207
212
  .color-button.ui.button {
208
213
  border: 1px solid rgba(34, 36, 38, 0.15);
209
214
  vertical-align: middle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@performant-software/semantic-components",
3
- "version": "1.0.19-beta.0",
3
+ "version": "1.0.19-beta.2",
4
4
  "description": "A package of shared components based on the Semantic UI Framework.",
5
5
  "license": "MIT",
6
6
  "main": "./build/index.js",
@@ -12,7 +12,7 @@
12
12
  "build": "webpack --mode production && flow-copy-source -v src types"
13
13
  },
14
14
  "dependencies": {
15
- "@performant-software/shared-components": "^1.0.19-beta.0",
15
+ "@performant-software/shared-components": "^1.0.19-beta.2",
16
16
  "@react-google-maps/api": "^2.8.1",
17
17
  "axios": "^0.26.1",
18
18
  "i18next": "^19.4.4",
File without changes
@@ -0,0 +1,69 @@
1
+ // @flow
2
+
3
+ import React, { useEffect, useState, type ComponentType } from 'react';
4
+ import { Breadcrumb, Loader } from 'semantic-ui-react';
5
+
6
+ type Props = {
7
+ active: boolean,
8
+ as?: ComponentType<any>,
9
+ id?: number,
10
+ label?: string,
11
+ name: string,
12
+ onLoad: (id: number, name: string) => Promise<any>,
13
+ url: string
14
+ };
15
+
16
+ const BreadcrumbItem: ComponentType<any> = (props: Props) => {
17
+ const [loading, setLoading] = useState(false);
18
+ const [name, setName] = useState(null);
19
+
20
+ /**
21
+ * Sets or clears the name attribute on the state.
22
+ */
23
+ useEffect(() => {
24
+ if (props.id) {
25
+ props
26
+ .onLoad(props.id, props.name)
27
+ .then((n) => setName(n))
28
+ .finally(() => setLoading(false));
29
+
30
+ setLoading(true);
31
+ } else {
32
+ setName(null);
33
+ }
34
+ }, [props.id, props.name]);
35
+
36
+ return (
37
+ <>
38
+ <Breadcrumb.Section
39
+ active={props.active && !props.id}
40
+ as={props.as}
41
+ to={`/${props.url}`}
42
+ >
43
+ { props.label }
44
+ </Breadcrumb.Section>
45
+ { props.id && (
46
+ <Breadcrumb.Divider
47
+ icon='right chevron'
48
+ />
49
+ )}
50
+ { loading && (
51
+ <Loader
52
+ active
53
+ inline
54
+ />
55
+ )}
56
+ { name && props.id && (
57
+ <Breadcrumb.Section
58
+ active={props.active}
59
+ as={props.as}
60
+ to={`/${props.url}/${props.id}`}
61
+ >
62
+ { name }
63
+ </Breadcrumb.Section>
64
+ )}
65
+ </>
66
+ );
67
+ };
68
+
69
+ export default BreadcrumbItem;
@@ -0,0 +1,4 @@
1
+ .ui.breadcrumb:first-child {
2
+ margin-bottom: 1em;
3
+ margin-top: 0.5em;
4
+ }
@@ -0,0 +1,111 @@
1
+ // @flow
2
+
3
+ import React, { useCallback, useMemo, type ComponentType } from 'react';
4
+ import { Breadcrumb } from 'semantic-ui-react';
5
+ import _ from 'underscore';
6
+ import BreadcrumbItem from './BreadcrumbItem';
7
+ import './Breadcrumbs.css';
8
+
9
+ const URL_DELIMITER = '/';
10
+
11
+ type Props = {
12
+ /**
13
+ * Alternate component to use to render the breadcrumb.
14
+ */
15
+ as?: ComponentType<any>,
16
+
17
+ /**
18
+ * A key-value pair of types to labels to match the `pathname`.
19
+ */
20
+ labels: { key: string, value: string },
21
+
22
+ /**
23
+ * Callback fired to lookup the name of the passed breadcrumb item.
24
+ */
25
+ onLoad: (id: number, name: string) => Promise<any>,
26
+
27
+ /**
28
+ * The URL for which to generate the breadcrumb.
29
+ */
30
+ pathname: string
31
+ };
32
+
33
+ /**
34
+ * This component can be used to render a breadcrumb for the passed URL.
35
+ */
36
+ const Breadcrumbs: ComponentType<any> = (props: Props) => {
37
+ /**
38
+ * Returns true if the passed string contains only digits.
39
+ *
40
+ * @type {function(*): boolean}
41
+ */
42
+ const isNumeric = useCallback((str: string) => /^\d+$/.test(str), []);
43
+
44
+ /**
45
+ * Sets the items to display based on the URL path.
46
+ *
47
+ * @type {[]}
48
+ */
49
+ const items = useMemo(() => {
50
+ const value = [];
51
+
52
+ const path = props.pathname.split(URL_DELIMITER).splice(1);
53
+
54
+ for (let i = 0; i < path.length; i += 1) {
55
+ const key = path[i];
56
+ const id = path[i + 1];
57
+ const url = path.slice(0, i + 1).join(URL_DELIMITER);
58
+
59
+ /*
60
+ * If the item in the path is non-numeric, we'll add it as the item label.
61
+ * If the next item is numeric, we'll add it as the ID.
62
+ * If the next item is non-numeric, it'll be added as a separate label on the next iteration.
63
+ */
64
+ if (!isNumeric(key)) {
65
+ const item = { key, url, id: undefined };
66
+
67
+ if (isNumeric(id)) {
68
+ item.id = id;
69
+ }
70
+
71
+ value.push(item);
72
+ }
73
+ }
74
+
75
+ return value;
76
+ }, [props.pathname]);
77
+
78
+ /**
79
+ * Returns true if there are more items to display.
80
+ *
81
+ * @type {function(*): boolean}
82
+ */
83
+ const hasMore = useCallback((index: number) => index < (items.length - 1), [items]);
84
+
85
+ return (
86
+ <Breadcrumb
87
+ size='large'
88
+ >
89
+ { _.map(items, (item, index) => (
90
+ <>
91
+ <BreadcrumbItem
92
+ active={!hasMore(index)}
93
+ as={props.as}
94
+ id={item.id}
95
+ label={props.labels[item.key]}
96
+ name={item.key}
97
+ onLoad={props.onLoad}
98
+ url={item.url}
99
+ />
100
+ { hasMore(index) && (
101
+ <Breadcrumb.Divider
102
+ icon='right chevron'
103
+ />
104
+ )}
105
+ </>
106
+ ))}
107
+ </Breadcrumb>
108
+ );
109
+ };
110
+
111
+ export default Breadcrumbs;
@@ -71,6 +71,15 @@ const useItemsToggle = (WrappedComponent: ComponentType<any>) => (
71
71
  return sort && sort.text;
72
72
  }
73
73
 
74
+ /**
75
+ * Returns true if the component should be hidden.
76
+ *
77
+ * @returns {boolean|*}
78
+ */
79
+ isHidden() {
80
+ return this.props.hideToggle && _.isEmpty(this.props.sort);
81
+ }
82
+
74
83
  /**
75
84
  * Calls the onSort prop.
76
85
  *
@@ -98,7 +107,7 @@ const useItemsToggle = (WrappedComponent: ComponentType<any>) => (
98
107
  * @returns {*}
99
108
  */
100
109
  render() {
101
- const renderListHeader = this.props.hideToggle
110
+ const renderListHeader = this.isHidden()
102
111
  ? undefined
103
112
  : this.renderHeader.bind(this);
104
113
 
@@ -121,27 +130,31 @@ const useItemsToggle = (WrappedComponent: ComponentType<any>) => (
121
130
  * @returns {*}
122
131
  */
123
132
  renderHeader() {
124
- if (this.props.hideToggle) {
133
+ if (this.isHidden()) {
125
134
  return null;
126
135
  }
127
136
 
128
137
  return (
129
138
  <>
130
- <Button
131
- active={this.state.view === Views.list}
132
- aria-label='List View'
133
- basic
134
- icon='list'
135
- onClick={() => this.setState({ view: Views.list })}
136
- />
137
- <Button
138
- active={this.state.view === Views.grid}
139
- aria-label='Grid View'
140
- basic
141
- icon='grid layout'
142
- onClick={() => this.setState({ view: Views.grid })}
143
- />
144
- { this.props.sort && this.props.sort.length > 1 && this.props.onSort && (
139
+ { !this.props.hideToggle && (
140
+ <>
141
+ <Button
142
+ active={this.state.view === Views.list}
143
+ aria-label='List View'
144
+ basic
145
+ icon='list'
146
+ onClick={() => this.setState({ view: Views.list })}
147
+ />
148
+ <Button
149
+ active={this.state.view === Views.grid}
150
+ aria-label='Grid View'
151
+ basic
152
+ icon='grid layout'
153
+ onClick={() => this.setState({ view: Views.grid })}
154
+ />
155
+ </>
156
+ )}
157
+ { !_.isEmpty(this.props.sort) && this.props.onSort && (
145
158
  <Button.Group
146
159
  basic
147
160
  style={{
package/src/i18n/en.json CHANGED
@@ -31,6 +31,11 @@
31
31
  "placeholder": "Enter a URL, ISBN, DOI, PMID, arXiv ID"
32
32
  }
33
33
  },
34
+ "BreadcrumbItem": {
35
+ "labels": {
36
+ "new": "New"
37
+ }
38
+ },
34
39
  "Citation": {
35
40
  "labels": {
36
41
  "untitled": "Untitled"
package/src/index.js CHANGED
@@ -14,6 +14,7 @@ export { default as BibliographyForm } from './components/BibliographyForm';
14
14
  export { default as BibliographyList } from './components/BibliographyList';
15
15
  export { default as BibliographySearchInput } from './components/BibliographySearchInput';
16
16
  export { default as BooleanIcon } from './components/BooleanIcon';
17
+ export { default as Breadcrumbs } from './components/Breadcrumbs';
17
18
  export { default as CancelButton } from './components/CancelButton';
18
19
  export { default as ColorButton } from './components/ColorButton';
19
20
  export { default as ColorPickerModal } from './components/ColorPickerModal';
@@ -0,0 +1,69 @@
1
+ // @flow
2
+
3
+ import React, { useEffect, useState, type ComponentType } from 'react';
4
+ import { Breadcrumb, Loader } from 'semantic-ui-react';
5
+
6
+ type Props = {
7
+ active: boolean,
8
+ as?: ComponentType<any>,
9
+ id?: number,
10
+ label?: string,
11
+ name: string,
12
+ onLoad: (id: number, name: string) => Promise<any>,
13
+ url: string
14
+ };
15
+
16
+ const BreadcrumbItem: ComponentType<any> = (props: Props) => {
17
+ const [loading, setLoading] = useState(false);
18
+ const [name, setName] = useState(null);
19
+
20
+ /**
21
+ * Sets or clears the name attribute on the state.
22
+ */
23
+ useEffect(() => {
24
+ if (props.id) {
25
+ props
26
+ .onLoad(props.id, props.name)
27
+ .then((n) => setName(n))
28
+ .finally(() => setLoading(false));
29
+
30
+ setLoading(true);
31
+ } else {
32
+ setName(null);
33
+ }
34
+ }, [props.id, props.name]);
35
+
36
+ return (
37
+ <>
38
+ <Breadcrumb.Section
39
+ active={props.active && !props.id}
40
+ as={props.as}
41
+ to={`/${props.url}`}
42
+ >
43
+ { props.label }
44
+ </Breadcrumb.Section>
45
+ { props.id && (
46
+ <Breadcrumb.Divider
47
+ icon='right chevron'
48
+ />
49
+ )}
50
+ { loading && (
51
+ <Loader
52
+ active
53
+ inline
54
+ />
55
+ )}
56
+ { name && props.id && (
57
+ <Breadcrumb.Section
58
+ active={props.active}
59
+ as={props.as}
60
+ to={`/${props.url}/${props.id}`}
61
+ >
62
+ { name }
63
+ </Breadcrumb.Section>
64
+ )}
65
+ </>
66
+ );
67
+ };
68
+
69
+ export default BreadcrumbItem;
@@ -0,0 +1,111 @@
1
+ // @flow
2
+
3
+ import React, { useCallback, useMemo, type ComponentType } from 'react';
4
+ import { Breadcrumb } from 'semantic-ui-react';
5
+ import _ from 'underscore';
6
+ import BreadcrumbItem from './BreadcrumbItem';
7
+ import './Breadcrumbs.css';
8
+
9
+ const URL_DELIMITER = '/';
10
+
11
+ type Props = {
12
+ /**
13
+ * Alternate component to use to render the breadcrumb.
14
+ */
15
+ as?: ComponentType<any>,
16
+
17
+ /**
18
+ * A key-value pair of types to labels to match the `pathname`.
19
+ */
20
+ labels: { key: string, value: string },
21
+
22
+ /**
23
+ * Callback fired to lookup the name of the passed breadcrumb item.
24
+ */
25
+ onLoad: (id: number, name: string) => Promise<any>,
26
+
27
+ /**
28
+ * The URL for which to generate the breadcrumb.
29
+ */
30
+ pathname: string
31
+ };
32
+
33
+ /**
34
+ * This component can be used to render a breadcrumb for the passed URL.
35
+ */
36
+ const Breadcrumbs: ComponentType<any> = (props: Props) => {
37
+ /**
38
+ * Returns true if the passed string contains only digits.
39
+ *
40
+ * @type {function(*): boolean}
41
+ */
42
+ const isNumeric = useCallback((str: string) => /^\d+$/.test(str), []);
43
+
44
+ /**
45
+ * Sets the items to display based on the URL path.
46
+ *
47
+ * @type {[]}
48
+ */
49
+ const items = useMemo(() => {
50
+ const value = [];
51
+
52
+ const path = props.pathname.split(URL_DELIMITER).splice(1);
53
+
54
+ for (let i = 0; i < path.length; i += 1) {
55
+ const key = path[i];
56
+ const id = path[i + 1];
57
+ const url = path.slice(0, i + 1).join(URL_DELIMITER);
58
+
59
+ /*
60
+ * If the item in the path is non-numeric, we'll add it as the item label.
61
+ * If the next item is numeric, we'll add it as the ID.
62
+ * If the next item is non-numeric, it'll be added as a separate label on the next iteration.
63
+ */
64
+ if (!isNumeric(key)) {
65
+ const item = { key, url, id: undefined };
66
+
67
+ if (isNumeric(id)) {
68
+ item.id = id;
69
+ }
70
+
71
+ value.push(item);
72
+ }
73
+ }
74
+
75
+ return value;
76
+ }, [props.pathname]);
77
+
78
+ /**
79
+ * Returns true if there are more items to display.
80
+ *
81
+ * @type {function(*): boolean}
82
+ */
83
+ const hasMore = useCallback((index: number) => index < (items.length - 1), [items]);
84
+
85
+ return (
86
+ <Breadcrumb
87
+ size='large'
88
+ >
89
+ { _.map(items, (item, index) => (
90
+ <>
91
+ <BreadcrumbItem
92
+ active={!hasMore(index)}
93
+ as={props.as}
94
+ id={item.id}
95
+ label={props.labels[item.key]}
96
+ name={item.key}
97
+ onLoad={props.onLoad}
98
+ url={item.url}
99
+ />
100
+ { hasMore(index) && (
101
+ <Breadcrumb.Divider
102
+ icon='right chevron'
103
+ />
104
+ )}
105
+ </>
106
+ ))}
107
+ </Breadcrumb>
108
+ );
109
+ };
110
+
111
+ export default Breadcrumbs;
@@ -71,6 +71,15 @@ const useItemsToggle = (WrappedComponent: ComponentType<any>) => (
71
71
  return sort && sort.text;
72
72
  }
73
73
 
74
+ /**
75
+ * Returns true if the component should be hidden.
76
+ *
77
+ * @returns {boolean|*}
78
+ */
79
+ isHidden() {
80
+ return this.props.hideToggle && _.isEmpty(this.props.sort);
81
+ }
82
+
74
83
  /**
75
84
  * Calls the onSort prop.
76
85
  *
@@ -98,7 +107,7 @@ const useItemsToggle = (WrappedComponent: ComponentType<any>) => (
98
107
  * @returns {*}
99
108
  */
100
109
  render() {
101
- const renderListHeader = this.props.hideToggle
110
+ const renderListHeader = this.isHidden()
102
111
  ? undefined
103
112
  : this.renderHeader.bind(this);
104
113
 
@@ -121,27 +130,31 @@ const useItemsToggle = (WrappedComponent: ComponentType<any>) => (
121
130
  * @returns {*}
122
131
  */
123
132
  renderHeader() {
124
- if (this.props.hideToggle) {
133
+ if (this.isHidden()) {
125
134
  return null;
126
135
  }
127
136
 
128
137
  return (
129
138
  <>
130
- <Button
131
- active={this.state.view === Views.list}
132
- aria-label='List View'
133
- basic
134
- icon='list'
135
- onClick={() => this.setState({ view: Views.list })}
136
- />
137
- <Button
138
- active={this.state.view === Views.grid}
139
- aria-label='Grid View'
140
- basic
141
- icon='grid layout'
142
- onClick={() => this.setState({ view: Views.grid })}
143
- />
144
- { this.props.sort && this.props.sort.length > 1 && this.props.onSort && (
139
+ { !this.props.hideToggle && (
140
+ <>
141
+ <Button
142
+ active={this.state.view === Views.list}
143
+ aria-label='List View'
144
+ basic
145
+ icon='list'
146
+ onClick={() => this.setState({ view: Views.list })}
147
+ />
148
+ <Button
149
+ active={this.state.view === Views.grid}
150
+ aria-label='Grid View'
151
+ basic
152
+ icon='grid layout'
153
+ onClick={() => this.setState({ view: Views.grid })}
154
+ />
155
+ </>
156
+ )}
157
+ { !_.isEmpty(this.props.sort) && this.props.onSort && (
145
158
  <Button.Group
146
159
  basic
147
160
  style={{
@@ -14,6 +14,7 @@ export { default as BibliographyForm } from './components/BibliographyForm';
14
14
  export { default as BibliographyList } from './components/BibliographyList';
15
15
  export { default as BibliographySearchInput } from './components/BibliographySearchInput';
16
16
  export { default as BooleanIcon } from './components/BooleanIcon';
17
+ export { default as Breadcrumbs } from './components/Breadcrumbs';
17
18
  export { default as CancelButton } from './components/CancelButton';
18
19
  export { default as ColorButton } from './components/ColorButton';
19
20
  export { default as ColorPickerModal } from './components/ColorPickerModal';