Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
899e9310
Commit
899e9310
authored
Mar 11, 2022
by
Lyza Danger Gardner
Committed by
Lyza Gardner
Mar 15, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor and improve design of tag lists and related components
parent
01cf0efe
Changes
13
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
242 additions
and
281 deletions
+242
-281
AnnotationBody.js
src/sidebar/components/Annotation/AnnotationBody.js
+61
-14
AnnotationBody-test.js
...sidebar/components/Annotation/test/AnnotationBody-test.js
+28
-4
TagEditor.js
src/sidebar/components/TagEditor.js
+9
-31
TagList.js
src/sidebar/components/TagList.js
+9
-51
TagListItem.js
src/sidebar/components/TagListItem.js
+66
-0
TagEditor-test.js
src/sidebar/components/test/TagEditor-test.js
+10
-10
TagList-test.js
src/sidebar/components/test/TagList-test.js
+0
-101
TagListItem-test.js
src/sidebar/components/test/TagListItem-test.js
+54
-0
icons.js
src/sidebar/icons.js
+4
-0
TagEditor.scss
src/styles/sidebar/components/TagEditor.scss
+0
-38
TagList.scss
src/styles/sidebar/components/TagList.scss
+0
-31
_index.scss
src/styles/sidebar/components/_index.scss
+0
-1
tailwind.config.mjs
tailwind.config.mjs
+1
-0
No files found.
src/sidebar/components/Annotation/AnnotationBody.js
View file @
899e9310
import
{
LabeledButton
}
from
'@hypothesis/frontend-shared'
;
import
{
Icon
,
LabeledButton
}
from
'@hypothesis/frontend-shared'
;
import
{
useState
}
from
'preact/hooks'
;
import
classnames
from
'classnames'
;
import
{
useMemo
,
useState
}
from
'preact/hooks'
;
import
{
useStoreProxy
}
from
'../../store/use-store'
;
import
{
useStoreProxy
}
from
'../../store/use-store'
;
import
{
isThirdPartyUser
}
from
'../../helpers/account-id'
;
import
{
isHidden
}
from
'../../helpers/annotation-metadata'
;
import
{
isHidden
}
from
'../../helpers/annotation-metadata'
;
import
{
withServices
}
from
'../../service-context'
;
import
{
withServices
}
from
'../../service-context'
;
import
{
applyTheme
}
from
'../../helpers/theme'
;
import
{
applyTheme
}
from
'../../helpers/theme'
;
...
@@ -9,6 +11,7 @@ import { applyTheme } from '../../helpers/theme';
...
@@ -9,6 +11,7 @@ import { applyTheme } from '../../helpers/theme';
import
Excerpt
from
'../Excerpt'
;
import
Excerpt
from
'../Excerpt'
;
import
MarkdownView
from
'../MarkdownView'
;
import
MarkdownView
from
'../MarkdownView'
;
import
TagList
from
'../TagList'
;
import
TagList
from
'../TagList'
;
import
TagListItem
from
'../TagListItem'
;
/**
/**
* @typedef {import("../../../types/api").Annotation} Annotation
* @typedef {import("../../../types/api").Annotation} Annotation
...
@@ -36,6 +39,7 @@ function AnnotationBody({ annotation, settings }) {
...
@@ -36,6 +39,7 @@ function AnnotationBody({ annotation, settings }) {
const
[
isCollapsible
,
setIsCollapsible
]
=
useState
(
false
);
const
[
isCollapsible
,
setIsCollapsible
]
=
useState
(
false
);
const
store
=
useStoreProxy
();
const
store
=
useStoreProxy
();
const
defaultAuthority
=
store
.
defaultAuthority
();
const
draft
=
store
.
getDraft
(
annotation
);
const
draft
=
store
.
getDraft
(
annotation
);
const
toggleText
=
isCollapsed
?
'More'
:
'Less'
;
const
toggleText
=
isCollapsed
?
'More'
:
'Less'
;
...
@@ -48,8 +52,20 @@ function AnnotationBody({ annotation, settings }) {
...
@@ -48,8 +52,20 @@ function AnnotationBody({ annotation, settings }) {
const
textStyle
=
applyTheme
([
'annotationFontFamily'
],
settings
);
const
textStyle
=
applyTheme
([
'annotationFontFamily'
],
settings
);
const
shouldLinkTags
=
useMemo
(
()
=>
annotation
&&
!
isThirdPartyUser
(
annotation
?.
user
,
defaultAuthority
),
[
annotation
,
defaultAuthority
]
);
/**
* @param {string} tag
*/
const
createTagSearchURL
=
tag
=>
{
return
store
.
getLink
(
'search.tag'
,
{
tag
});
};
return
(
return
(
<
div
className
=
"
hyp-u-vertical-spacing--2
"
>
<
div
className
=
"
space-y-4
"
>
{
showExcerpt
&&
(
{
showExcerpt
&&
(
<
Excerpt
<
Excerpt
collapse
=
{
isCollapsed
}
collapse
=
{
isCollapsed
}
...
@@ -63,24 +79,55 @@ function AnnotationBody({ annotation, settings }) {
...
@@ -63,24 +79,55 @@ function AnnotationBody({ annotation, settings }) {
textStyle
=
{
textStyle
}
textStyle
=
{
textStyle
}
markdown
=
{
text
}
markdown
=
{
text
}
textClass
=
{{
textClass
=
{{
AnnotationBody__text
:
true
,
'p-redacted-content'
:
isHidden
(
annotation
),
'p-redacted-content'
:
isHidden
(
annotation
),
}}
}}
/
>
/
>
<
/Excerpt
>
<
/Excerpt
>
)}
)}
{(
isCollapsible
||
showTagList
)
&&
(
<
div
className
=
"flex flex-row gap-x-2"
>
<
div
className
=
"grow"
>
{
showTagList
&&
(
<
TagList
>
{
tags
.
map
(
tag
=>
{
return
(
<
TagListItem
key
=
{
tag
}
tag
=
{
tag
}
href
=
{
shouldLinkTags
?
createTagSearchURL
(
tag
)
:
undefined
}
/
>
);
})}
<
/TagList
>
)}
<
/div
>
{
isCollapsible
&&
(
{
isCollapsible
&&
(
<
div
className
=
"hyp-u-layout-row--justify-right"
>
<
div
>
<
LabeledButton
<
LabeledButton
classes
=
{
classnames
(
// Pull this button up toward the bottom of the excerpt content
'-mt-3'
,
'text-grey-7 font-normal'
)}
expanded
=
{
!
isCollapsed
}
expanded
=
{
!
isCollapsed
}
onClick
=
{()
=>
setIsCollapsed
(
!
isCollapsed
)}
onClick
=
{()
=>
setIsCollapsed
(
!
isCollapsed
)}
title
=
{
`Toggle visibility of full annotation text: Show
${
toggleText
}
`
}
title
=
{
`Toggle visibility of full annotation text: Show
${
toggleText
}
`
}
>
>
{
toggleText
}
<
div
className
=
"flex items-center gap-x-2"
>
<
Icon
classes
=
"!text-tiny"
name
=
{
isCollapsed
?
'expand'
:
'collapse'
}
title
=
{
isCollapsed
?
'expand'
:
'collapse'
}
/
>
<
div
>
{
toggleText
}
<
/div
>
<
/div
>
<
/LabeledButton
>
<
/LabeledButton
>
<
/div
>
<
/div
>
)}
)}
{
showTagList
&&
<
TagList
annotation
=
{
annotation
}
tags
=
{
tags
}
/>
}
<
/div
>
)}
<
/div
>
<
/div
>
);
);
}
}
...
...
src/sidebar/components/Annotation/test/AnnotationBody-test.js
View file @
899e9310
...
@@ -11,6 +11,7 @@ import AnnotationBody, { $imports } from '../AnnotationBody';
...
@@ -11,6 +11,7 @@ import AnnotationBody, { $imports } from '../AnnotationBody';
describe
(
'AnnotationBody'
,
()
=>
{
describe
(
'AnnotationBody'
,
()
=>
{
let
fakeAnnotation
;
let
fakeAnnotation
;
let
fakeApplyTheme
;
let
fakeApplyTheme
;
let
fakeIsThirdPartyUser
;
let
fakeSettings
;
let
fakeSettings
;
// Inject dependency mocks
// Inject dependency mocks
...
@@ -44,14 +45,20 @@ describe('AnnotationBody', () => {
...
@@ -44,14 +45,20 @@ describe('AnnotationBody', () => {
fakeAnnotation
.
text
=
'some text here'
;
fakeAnnotation
.
text
=
'some text here'
;
fakeAnnotation
.
tags
=
[
'eenie'
,
'minie'
];
fakeAnnotation
.
tags
=
[
'eenie'
,
'minie'
];
fakeApplyTheme
=
sinon
.
stub
();
fakeApplyTheme
=
sinon
.
stub
();
fakeIsThirdPartyUser
=
sinon
.
stub
().
returns
(
false
);
fakeSettings
=
{};
fakeSettings
=
{};
fakeStore
=
{
fakeStore
=
{
defaultAuthority
:
sinon
.
stub
(),
getDraft
:
sinon
.
stub
().
returns
(
null
),
getDraft
:
sinon
.
stub
().
returns
(
null
),
getLink
:
sinon
.
stub
()
.
callsFake
((
linkPath
,
{
tag
})
=>
`http://www.example.com/
${
tag
}
`
),
};
};
$imports
.
$mock
(
mockImportedComponents
());
$imports
.
$mock
(
mockImportedComponents
());
$imports
.
$mock
({
$imports
.
$mock
({
'../../helpers/account-id'
:
{
isThirdPartyUser
:
fakeIsThirdPartyUser
},
'../../helpers/theme'
:
{
applyTheme
:
fakeApplyTheme
},
'../../helpers/theme'
:
{
applyTheme
:
fakeApplyTheme
},
'../../store/use-store'
:
{
useStoreProxy
:
()
=>
fakeStore
},
'../../store/use-store'
:
{
useStoreProxy
:
()
=>
fakeStore
},
});
});
...
@@ -66,9 +73,25 @@ describe('AnnotationBody', () => {
...
@@ -66,9 +73,25 @@ describe('AnnotationBody', () => {
wrapper
.
update
();
wrapper
.
update
();
const
markdownView
=
wrapper
.
find
(
'MarkdownView'
);
const
markdownView
=
wrapper
.
find
(
'MarkdownView'
);
const
tag
List
=
wrapper
.
find
(
'TagList
'
);
const
tag
Items
=
wrapper
.
find
(
'TagListItem
'
);
assert
.
strictEqual
(
markdownView
.
props
().
markdown
,
'some text here'
);
assert
.
strictEqual
(
markdownView
.
props
().
markdown
,
'some text here'
);
assert
.
deepStrictEqual
(
tagList
.
props
().
tags
,
[
'eenie'
,
'minie'
]);
assert
.
equal
(
tagItems
.
length
,
fakeAnnotation
.
tags
.
length
);
assert
.
equal
(
tagItems
.
first
().
props
().
tag
,
'eenie'
);
});
it
(
'links the annotation tags'
,
()
=>
{
const
wrapper
=
createBody
();
const
tagItems
=
wrapper
.
find
(
'TagListItem'
);
assert
.
equal
(
tagItems
.
first
().
props
().
href
,
'http://www.example.com/eenie'
);
});
it
(
'does not link the annotation tags if the user is third party'
,
()
=>
{
fakeIsThirdPartyUser
.
returns
(
true
);
const
wrapper
=
createBody
();
const
tagItems
=
wrapper
.
find
(
'TagListItem'
);
assert
.
isUndefined
(
tagItems
.
first
().
props
().
href
);
});
});
it
(
'renders the tags and text from the draft'
,
()
=>
{
it
(
'renders the tags and text from the draft'
,
()
=>
{
...
@@ -78,9 +101,10 @@ describe('AnnotationBody', () => {
...
@@ -78,9 +101,10 @@ describe('AnnotationBody', () => {
wrapper
.
update
();
wrapper
.
update
();
const
markdownView
=
wrapper
.
find
(
'MarkdownView'
);
const
markdownView
=
wrapper
.
find
(
'MarkdownView'
);
const
tag
List
=
wrapper
.
find
(
'TagList
'
);
const
tag
Items
=
wrapper
.
find
(
'TagListItem
'
);
assert
.
strictEqual
(
markdownView
.
props
().
markdown
,
'this is a draft'
);
assert
.
strictEqual
(
markdownView
.
props
().
markdown
,
'this is a draft'
);
assert
.
deepStrictEqual
(
tagList
.
props
().
tags
,
[
'1'
,
'2'
]);
assert
.
equal
(
tagItems
.
length
,
2
);
assert
.
equal
(
tagItems
.
first
().
props
().
tag
,
'1'
);
});
});
it
(
'does not render controls to expand/collapse the excerpt if it is not collapsible'
,
()
=>
{
it
(
'does not render controls to expand/collapse the excerpt if it is not collapsible'
,
()
=>
{
...
...
src/sidebar/components/TagEditor.js
View file @
899e9310
import
{
import
{
Icon
,
normalizeKeyName
,
normalizeKeyName
,
useElementShouldClose
,
useElementShouldClose
,
}
from
'@hypothesis/frontend-shared'
;
}
from
'@hypothesis/frontend-shared'
;
...
@@ -8,6 +7,8 @@ import { useRef, useState } from 'preact/hooks';
...
@@ -8,6 +7,8 @@ import { useRef, useState } from 'preact/hooks';
import
{
withServices
}
from
'../service-context'
;
import
{
withServices
}
from
'../service-context'
;
import
AutocompleteList
from
'./AutocompleteList'
;
import
AutocompleteList
from
'./AutocompleteList'
;
import
TagList
from
'./TagList'
;
import
TagListItem
from
'./TagListItem'
;
/** @typedef {import("preact").JSX.Element} JSXElement */
/** @typedef {import("preact").JSX.Element} JSXElement */
...
@@ -48,7 +49,7 @@ function TagEditor({
...
@@ -48,7 +49,7 @@ function TagEditor({
});
});
// Set up callback to monitor outside click events to close the AutocompleteList
// Set up callback to monitor outside click events to close the AutocompleteList
const
closeWrapperRef
=
/** @type {{ current: HTMLElement }} */
(
useRef
());
const
closeWrapperRef
=
/** @type {{ current: HTML
Div
Element }} */
(
useRef
());
useElementShouldClose
(
closeWrapperRef
,
suggestionsListOpen
,
()
=>
{
useElementShouldClose
(
closeWrapperRef
,
suggestionsListOpen
,
()
=>
{
setSuggestionsListOpen
(
false
);
setSuggestionsListOpen
(
false
);
});
});
...
@@ -269,36 +270,13 @@ function TagEditor({
...
@@ -269,36 +270,13 @@ function TagEditor({
activeItem
>=
0
?
`
${
tagEditorId
}
-AutocompleteList-item-
${
activeItem
}
`
:
''
;
activeItem
>=
0
?
`
${
tagEditorId
}
-AutocompleteList-item-
${
activeItem
}
`
:
''
;
return
(
return
(
<
div
className
=
"TagEditor"
>
<
div
className
=
"TagEditor space-y-4"
>
<
ul
<
TagList
>
className
=
"TagEditor__tags"
aria
-
label
=
"Suggested tags for annotation"
>
{
tagList
.
map
(
tag
=>
{
{
tagList
.
map
(
tag
=>
{
return
(
return
<
TagListItem
key
=
{
tag
}
onRemoveTag
=
{
onRemoveTag
}
tag
=
{
tag
}
/>
;
<
li
key
=
{
`
${
tag
}
`
}
className
=
"TagEditor__item"
aria
-
label
=
{
`Tag:
${
tag
}
`
}
>
<
span
lang
=
""
className
=
"TagEditor__edit"
>
{
tag
}
<
/span
>
<
button
onClick
=
{()
=>
{
onRemoveTag
(
tag
);
}}
aria
-
label
=
{
`Remove Tag:
${
tag
}
`
}
title
=
{
`Remove Tag:
${
tag
}
`
}
className
=
"TagEditor__delete"
>
<
Icon
name
=
"cancel"
/>
<
/button
>
<
/li
>
);
})}
})}
<
/
ul
>
<
/
TagList
>
<
span
<
div
id
=
{
tagEditorId
}
id
=
{
tagEditorId
}
className
=
"TagEditor__combobox-wrapper"
className
=
"TagEditor__combobox-wrapper"
ref
=
{
closeWrapperRef
}
ref
=
{
closeWrapperRef
}
...
@@ -332,7 +310,7 @@ function TagEditor({
...
@@ -332,7 +310,7 @@ function TagEditor({
itemPrefixId
=
{
`
${
tagEditorId
}
-AutocompleteList-item-`
}
itemPrefixId
=
{
`
${
tagEditorId
}
-AutocompleteList-item-`
}
activeItem
=
{
activeItem
}
activeItem
=
{
activeItem
}
/
>
/
>
<
/
span
>
<
/
div
>
<
/div
>
<
/div
>
);
);
}
}
...
...
src/sidebar/components/TagList.js
View file @
899e9310
import
{
useMemo
}
from
'preact/hooks'
;
import
{
useStoreProxy
}
from
'../store/use-store'
;
import
{
isThirdPartyUser
}
from
'../helpers/account-id'
;
/** @typedef {import('../../types/api').Annotation} Annotation */
/**
/**
* @typedef TagListProps
* @typedef TagListProps
* @prop {Annotation} annotation - Annotation that owns the tags.
* @prop {import("preact").ComponentChildren} children
* @prop {string[]} tags - List of tags as strings.
*/
*/
/**
/**
* Component to render an annotation's tags.
* Render a list container for a list of annotation tags.
*
* @param {TagListProps} props
* @param {TagListProps} props
*/
*/
function
TagList
({
annotation
,
tags
})
{
function
TagList
({
children
})
{
const
store
=
useStoreProxy
();
const
defaultAuthority
=
store
.
defaultAuthority
();
const
renderLink
=
useMemo
(
// Show a link if the authority of the user is not 3rd party
()
=>
!
isThirdPartyUser
(
annotation
.
user
,
defaultAuthority
),
[
annotation
,
defaultAuthority
]
);
/**
* Returns a uri link for a specific tag name.
* @param {string} tag
* @return {string}
*/
const
createTagSearchURL
=
tag
=>
{
return
store
.
getLink
(
'search.tag'
,
{
tag
});
};
return
(
return
(
<
ul
className
=
"TagList"
aria
-
label
=
"Annotation tags"
>
<
ul
{
tags
.
map
(
tag
=>
(
className
=
"flex flex-wrap gap-2 leading-none"
<
li
key
=
{
tag
}
className
=
"TagList__item"
>
aria
-
label
=
"Annotation tags"
{
renderLink
&&
(
<
a
className
=
"TagList__link"
href
=
{
createTagSearchURL
(
tag
)}
lang
=
""
target
=
"_blank"
rel
=
"noopener noreferrer"
aria
-
label
=
{
`Tag:
${
tag
}
`
}
title
=
{
`View annotations with tag:
${
tag
}
`
}
>
>
{
tag
}
{
children
}
<
/a
>
)}
{
!
renderLink
&&
(
<
span
className
=
"TagList__text"
aria
-
label
=
{
`Tag:
${
tag
}
`
}
lang
=
""
>
{
tag
}
<
/span
>
)}
<
/li
>
))}
<
/ul
>
<
/ul
>
);
);
}
}
...
...
src/sidebar/components/TagListItem.js
0 → 100644
View file @
899e9310
import
{
Icon
,
Link
}
from
'@hypothesis/frontend-shared'
;
import
classnames
from
'classnames'
;
/**
* @typedef TagListItemProps
* @prop {string} [href] - If present, tag will be linked to this URL
* @prop {(tag: string) => void} [onRemoveTag] - Callback for deleting a tag. If
* present, a delete button will be provided for the tag
* @prop {string} tag
*/
/**
* Render a single annotation tag as part of a list of tags
*
* @param {TagListItemProps} props
*/
export
default
function
TagListItem
({
href
,
onRemoveTag
,
tag
})
{
return
(
<
li
className
=
"flex items-center border rounded-sm bg-grey-0"
>
<
div
className
=
"grow px-1.5 py-1 touch:p-2"
>
{
href
?
(
<
Link
classes
=
"text-color-text-light hover:text-brand"
href
=
{
href
}
lang
=
""
target
=
"_blank"
aria
-
label
=
{
`Tag:
${
tag
}
`
}
title
=
{
`View annotations with tag:
${
tag
}
`
}
>
{
tag
}
<
/Link
>
)
:
(
<
span
className
=
"text-color-text-light cursor-default"
aria
-
label
=
{
`Tag:
${
tag
}
`
}
lang
=
""
>
{
tag
}
<
/span
>
)}
<
/div
>
{
onRemoveTag
&&
(
<
button
className
=
{
classnames
(
// More padding for mobile users to make touch target larger
'px-1.5 py-1 touch:p-2'
,
// Rounded border to match container edges and make keyboard focus
// ring shape conform. Turn off left side
// border radius to maintain a straight dividing line
'border-l rounded-sm rounded-l-none'
,
'text-grey-6 hover:text-color-text hover:bg-grey-2'
,
// Emulates transitions on *Button shared component styling
'transition-colors duration-200'
,
'hyp-u-outline-on-keyboard-focus--inset'
)}
onClick
=
{()
=>
{
onRemoveTag
(
tag
);
}}
title
=
{
`Remove tag:
${
tag
}
`
}
>
<
Icon
classes
=
"font-base"
name
=
"cancel"
title
=
{
`Remove
${
tag
}
`
}
/
>
<
/button
>
)}
<
/li
>
);
}
src/sidebar/components/test/TagEditor-test.js
View file @
899e9310
...
@@ -390,16 +390,16 @@ describe('TagEditor', () => {
...
@@ -390,16 +390,16 @@ describe('TagEditor', () => {
});
});
});
});
describe
(
'when removing tags'
,
()
=>
{
describe
(
'tag removal'
,
()
=>
{
it
(
'removes `tag1` when clicking its delete button'
,
()
=>
{
it
(
'provides a tag removal callback to each tag in the tag list'
,
()
=>
{
const
wrapper
=
createComponent
();
// note: initial tagList is ['tag1', 'tag2']
const
wrapper
=
createComponent
();
assert
.
equal
(
wrapper
.
find
(
'.TagEditor__edit'
).
length
,
2
);
const
tagListItems
=
wrapper
.
find
(
'TagListItem'
);
wrapper
.
find
(
'button'
)
.
at
(
0
)
// delete 'tag1'
.
simulate
(
'click'
);
assert
.
calledWith
(
fakeOnRemoveTag
,
'tag1'
);
assert
.
equal
(
tagListItems
.
length
,
2
);
assert
.
equal
(
tagListItems
.
first
().
props
().
onRemoveTag
,
wrapper
.
props
().
onRemoveTag
);
});
});
});
});
...
...
src/sidebar/components/test/TagList-test.js
deleted
100644 → 0
View file @
01cf0efe
import
{
mount
}
from
'enzyme'
;
import
TagList
,
{
$imports
}
from
'../TagList'
;
import
{
checkAccessibility
}
from
'../../../test-util/accessibility'
;
import
{
mockImportedComponents
}
from
'../../../test-util/mock-imported-components'
;
describe
(
'TagList'
,
()
=>
{
let
fakeIsThirdPartyUser
;
let
fakeStore
;
const
fakeTags
=
[
'tag1'
,
'tag2'
];
function
createComponent
(
props
)
{
return
mount
(
<
TagList
annotation
=
{{}}
tags
=
{
fakeTags
}
{...
props
}
/>
)
;
}
beforeEach
(()
=>
{
fakeIsThirdPartyUser
=
sinon
.
stub
().
returns
(
false
);
fakeStore
=
{
defaultAuthority
:
sinon
.
stub
().
returns
(
'hypothes.is'
),
getLink
:
sinon
.
stub
().
returns
(
'http://serviceurl.com'
),
};
$imports
.
$mock
(
mockImportedComponents
());
$imports
.
$mock
({
'../helpers/account-id'
:
{
isThirdPartyUser
:
fakeIsThirdPartyUser
,
},
'../store/use-store'
:
{
useStoreProxy
:
()
=>
fakeStore
},
});
});
afterEach
(()
=>
{
$imports
.
$restore
();
});
it
(
'does not render any tags if `tags` prop is empty'
,
()
=>
{
const
wrapper
=
createComponent
({
tags
:
[]
});
assert
.
isFalse
(
wrapper
.
find
(
'.TagList__item a'
).
exists
());
});
context
(
'when `isThirdPartyUser` returns false'
,
()
=>
{
it
(
'adds appropriate classes, props and values'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'a'
).
forEach
((
link
,
i
)
=>
{
assert
.
isTrue
(
link
.
hasClass
(
'TagList__link'
));
assert
.
equal
(
link
.
prop
(
'aria-label'
),
`Tag:
${
fakeTags
[
i
]}
`
);
assert
.
equal
(
link
.
prop
(
'href'
),
'http://serviceurl.com'
);
assert
.
equal
(
link
.
prop
(
'title'
),
`View annotations with tag:
${
fakeTags
[
i
]}
`
);
assert
.
equal
(
link
.
text
(),
fakeTags
[
i
]);
});
});
it
(
'gets the links for tags'
,
()
=>
{
createComponent
();
assert
.
calledWith
(
fakeStore
.
getLink
,
'search.tag'
,
{
tag
:
'tag1'
});
assert
.
calledWith
(
fakeStore
.
getLink
,
'search.tag'
,
{
tag
:
'tag2'
});
});
});
context
(
'when `isThirdPartyUser` returns true'
,
()
=>
{
beforeEach
(()
=>
{
fakeIsThirdPartyUser
.
returns
(
true
);
});
it
(
'adds appropriate classes, props and values'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'span'
).
forEach
((
link
,
i
)
=>
{
assert
.
isTrue
(
link
.
hasClass
(
'TagList__text'
));
assert
.
equal
(
link
.
prop
(
'aria-label'
),
`Tag:
${
fakeTags
[
i
]}
`
);
assert
.
equal
(
link
.
text
(),
fakeTags
[
i
]);
});
});
it
(
'does not fetch tag link'
,
()
=>
{
createComponent
();
assert
.
notCalled
(
fakeStore
.
getLink
);
});
});
it
(
'should pass a11y checks'
,
checkAccessibility
([
{
name
:
'first-party user'
,
content
:
()
=>
createComponent
({
tags
:
[
'tag1'
,
'tag2'
]
}),
},
{
name
:
'third-party user'
,
content
:
()
=>
{
fakeIsThirdPartyUser
.
returns
(
true
);
return
createComponent
({
tags
:
[
'tag1'
,
'tag2'
]
});
},
},
])
);
});
src/sidebar/components/test/TagListItem-test.js
0 → 100644
View file @
899e9310
import
{
mount
}
from
'enzyme'
;
import
TagListItem
from
'../TagListItem'
;
import
TagList
from
'../TagList'
;
import
{
checkAccessibility
}
from
'../../../test-util/accessibility'
;
describe
(
'TagListItem'
,
()
=>
{
const
createComponent
=
props
=>
mount
(
<
TagListItem
tag
=
"my tag"
{...
props
}
/>
)
;
// Render TagListItems in a list to appease semantic requirements of
// a11y tests
const
createComponentInList
=
()
=>
mount
(
<
TagList
>
<
TagListItem
tag
=
"my tag"
href
=
"http://www.example.com/my-tag"
/>
<
TagListItem
tag
=
"purple"
/>
<
/TagList
>
);
it
(
'renders the tag text'
,
()
=>
{
const
wrapper
=
createComponent
();
assert
.
equal
(
wrapper
.
text
(),
'my tag'
);
});
it
(
'links the tag to the provided `href`'
,
()
=>
{
const
wrapper
=
createComponent
({
href
:
'http:www.example.com/my-tag'
});
const
link
=
wrapper
.
find
(
'Link'
);
assert
.
equal
(
link
.
props
().
href
,
'http:www.example.com/my-tag'
);
});
it
(
'renders a delete button if a removal callback is provided'
,
()
=>
{
const
onRemoveTag
=
sinon
.
stub
();
const
wrapper
=
createComponent
({
tag
:
'banana'
,
onRemoveTag
});
assert
.
isTrue
(
wrapper
.
find
(
'button[title="Remove tag: banana"]'
).
exists
());
});
it
(
'invokes the removal callback when the delete button is clicked'
,
()
=>
{
const
onRemoveTag
=
sinon
.
stub
();
const
wrapper
=
createComponent
({
onRemoveTag
});
wrapper
.
find
(
'button'
).
simulate
(
'click'
);
assert
.
calledOnce
(
onRemoveTag
);
});
it
(
'should pass a11y checks'
,
checkAccessibility
({
content
:
()
=>
createComponentInList
(),
})
);
});
src/sidebar/icons.js
View file @
899e9310
...
@@ -9,6 +9,7 @@ import {
...
@@ -9,6 +9,7 @@ import {
ccStd
,
ccStd
,
ccZero
,
ccZero
,
check
,
check
,
collapse
,
copy
,
copy
,
edit
,
edit
,
editorLatex
,
editorLatex
,
...
@@ -16,6 +17,7 @@ import {
...
@@ -16,6 +17,7 @@ import {
editorTextBold
,
editorTextBold
,
editorTextItalic
,
editorTextItalic
,
email
,
email
,
expand
,
external
,
external
,
flag
,
flag
,
flagFilled
,
flagFilled
,
...
@@ -67,12 +69,14 @@ export const sidebarIcons = {
...
@@ -67,12 +69,14 @@ export const sidebarIcons = {
'cc-std'
:
ccStd
,
'cc-std'
:
ccStd
,
'cc-zero'
:
ccZero
,
'cc-zero'
:
ccZero
,
'collapse-menu'
:
caretUp
,
'collapse-menu'
:
caretUp
,
collapse
,
collapsed
:
caretRight
,
collapsed
:
caretRight
,
copy
,
copy
,
edit
,
edit
,
email
,
email
,
'expand-menu'
:
expandMenuIcon
,
'expand-menu'
:
expandMenuIcon
,
error
:
cancel
,
error
:
cancel
,
expand
,
external
,
external
,
facebook
:
socialFacebook
,
facebook
:
socialFacebook
,
flag
,
flag
,
...
...
src/styles/sidebar/components/TagEditor.scss
View file @
899e9310
@use
'../../mixins/forms'
;
@use
'../../mixins/forms'
;
@use
'../../mixins/buttons'
;
@use
'../../mixins/layout'
;
@use
'../../mixins/utils'
;
@use
'../../variables'
as
var
;
.TagEditor
{
.TagEditor
{
@include
layout
.
vertical-rhythm
(
var
.
$layout-space--xsmall
);
&
__input
{
&
__input
{
@include
forms
.
form-input
;
@include
forms
.
form-input
;
width
:
100%
;
width
:
100%
;
}
}
&
__tags
{
@include
layout
.
row
;
flex-wrap
:
wrap
;
}
&
__item
{
@include
layout
.
row
;
margin
:
var
.
$layout-space--xxsmall
var
.
$layout-space--xsmall
var
.
$layout-space--xsmall
0
;
}
&
__edit
{
@include
utils
.
border
;
color
:
var
.
$color-text
;
background
:
var
.
$grey-1
;
border-radius
:
var
.
$border-radius
0
0
var
.
$border-radius
;
border-right-width
:
0
;
padding
:
2px
var
.
$layout-space--xsmall
;
}
&
__delete
{
@include
buttons
.
button
;
@include
utils
.
border
;
color
:
var
.
$grey-mid
;
background-color
:
var
.
$grey-1
;
padding
:
0
var
.
$layout-space--xsmall
;
border-radius
:
0
var
.
$border-radius
var
.
$border-radius
0
;
&
:hover
{
background-color
:
var
.
$grey-2
;
}
}
}
}
src/styles/sidebar/components/TagList.scss
deleted
100644 → 0
View file @
01cf0efe
@use
'../../variables'
as
var
;
@use
'../../mixins/layout'
;
@use
'../../mixins/utils'
;
.TagList
{
@include
layout
.
row
;
flex-wrap
:
wrap
;
&
__item
{
@include
utils
.
border
;
margin
:
var
.
$layout-space--xxsmall
var
.
$layout-space--xsmall
var
.
$layout-space--xxsmall
0
;
padding
:
2px
var
.
$layout-space--xsmall
;
background
:
var
.
$grey-0
;
border-radius
:
var
.
$border-radius
;
}
&
__link
,
&
__text
{
text-decoration
:
none
;
color
:
var
.
$color-text--light
;
}
&
__link
{
&
:hover
,
&
:focus
{
color
:
var
.
$color-brand
;
border-color
:
var
.
$grey-4
;
}
}
}
src/styles/sidebar/components/_index.scss
View file @
899e9310
...
@@ -33,7 +33,6 @@
...
@@ -33,7 +33,6 @@
@use
'./SelectionTabs'
;
@use
'./SelectionTabs'
;
@use
'./SearchInput'
;
@use
'./SearchInput'
;
@use
'./TagEditor'
;
@use
'./TagEditor'
;
@use
'./TagList'
;
@use
'./Thread'
;
@use
'./Thread'
;
@use
'./ThreadCard'
;
@use
'./ThreadCard'
;
@use
'./ThreadList'
;
@use
'./ThreadList'
;
...
...
tailwind.config.mjs
View file @
899e9310
...
@@ -104,6 +104,7 @@ export default {
...
@@ -104,6 +104,7 @@ export default {
},
},
},
},
screens
:
{
screens
:
{
touch
:
{
raw
:
'(pointer: coarse)'
},
md
:
'480px'
,
md
:
'480px'
,
// Narrow mobile screens
// Narrow mobile screens
'annotator-sm'
:
'240px'
,
'annotator-sm'
:
'240px'
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment