Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07ec3486a0 | ||
|
|
ed0c73e051 | ||
|
|
1041b3e45f | ||
|
|
493a0610ca | ||
|
|
679e21c270 | ||
|
|
62eb9f5e02 | ||
|
|
dedbc25358 | ||
|
|
0f2ad8b7b4 | ||
|
|
61581ff474 | ||
|
|
3afce17a32 | ||
|
|
7e36e9f14c |
@@ -16,4 +16,4 @@ jobs:
|
|||||||
secrets:
|
secrets:
|
||||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||||
|
|||||||
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# This workflow runs when a comment is made on the ticket
|
||||||
|
# If the comment starts with "label: " it tries to apply
|
||||||
|
# the label indicated in rest of comment.
|
||||||
|
# If the comment starts with "remove label: ", it tries
|
||||||
|
# to remove the indicated label.
|
||||||
|
# Note: Labels are allowed to have spaces and this script does
|
||||||
|
# not parse spaces (as often a space is legitimate), so the command
|
||||||
|
# "label: really long lots of words label" will apply the
|
||||||
|
# label "really long lots of words label"
|
||||||
|
|
||||||
|
name: Allows for the adding and removing of labels via comment
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add_remove_labels:
|
||||||
|
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
|
||||||
|
|
||||||
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# This workflow runs when a comment is made on the ticket
|
||||||
|
# If the comment starts with "assign me" it assigns the author to the
|
||||||
|
# ticket (case insensitive)
|
||||||
|
|
||||||
|
name: Assign comment author to ticket if they say "assign me"
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
self_assign_by_comment:
|
||||||
|
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script
|
<script
|
||||||
async
|
defer
|
||||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
|
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
|
||||||
id="MathJax-script"
|
id="MathJax-script"
|
||||||
></script>
|
></script>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<div id="root" class="small"></div>
|
<div id="root" class="small"></div>
|
||||||
|
|
||||||
<!-- begin usabilla live embed code -->
|
<!-- begin usabilla live embed code -->
|
||||||
<script type="text/javascript">
|
<script defer type="text/javascript">
|
||||||
window.lightningjs ||
|
window.lightningjs ||
|
||||||
(function (n) {
|
(function (n) {
|
||||||
var e = "lightningjs";
|
var e = "lightningjs";
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export default function DiscussionsHome() {
|
|||||||
</Switch>
|
</Switch>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DiscussionsProductTour />
|
{!enableInContextSidebar && <DiscussionsProductTour />}
|
||||||
</main>
|
</main>
|
||||||
{!enableInContextSidebar && <Footer />}
|
{!enableInContextSidebar && <Footer />}
|
||||||
</DiscussionContext.Provider>
|
</DiscussionContext.Provider>
|
||||||
|
|||||||
255
src/discussions/in-context-topics/TopicPostsView.test.jsx
Normal file
255
src/discussions/in-context-topics/TopicPostsView.test.jsx
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import {
|
||||||
|
fireEvent, render, screen, waitFor, within,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { IntlProvider } from 'react-intl';
|
||||||
|
import { generatePath, MemoryRouter, Route } from 'react-router';
|
||||||
|
import { Factory } from 'rosie';
|
||||||
|
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
|
import { PostActionsBar } from '../../components';
|
||||||
|
import { Routes } from '../../data/constants';
|
||||||
|
import { initializeStore } from '../../store';
|
||||||
|
import { executeThunk } from '../../test-utils';
|
||||||
|
import { DiscussionContext } from '../common/context';
|
||||||
|
import { getThreadsApiUrl } from '../posts/data/api';
|
||||||
|
import { fetchThreads } from '../posts/data/thunks';
|
||||||
|
import { getCourseTopicsApiUrl } from './data/api';
|
||||||
|
import { selectCoursewareTopics } from './data/selectors';
|
||||||
|
import { fetchCourseTopicsV3 } from './data/thunks';
|
||||||
|
import TopicPostsView from './TopicPostsView';
|
||||||
|
import TopicsView from './TopicsView';
|
||||||
|
|
||||||
|
import './data/__factories__';
|
||||||
|
import '../posts/data/__factories__/threads.factory';
|
||||||
|
|
||||||
|
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
|
const threadsApiUrl = getThreadsApiUrl();
|
||||||
|
const topicsApiUrl = getCourseTopicsApiUrl();
|
||||||
|
let store;
|
||||||
|
let axiosMock;
|
||||||
|
let lastLocation;
|
||||||
|
let container;
|
||||||
|
|
||||||
|
async function renderComponent({ topicId, category } = { }) {
|
||||||
|
let path = `/${courseId}/topics`;
|
||||||
|
if (topicId) {
|
||||||
|
path = generatePath(Routes.POSTS.PATH, { courseId, topicId });
|
||||||
|
} else if (category) {
|
||||||
|
path = generatePath(Routes.TOPICS.CATEGORY, { courseId, category });
|
||||||
|
}
|
||||||
|
const wrapper = await render(
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<DiscussionContext.Provider value={{
|
||||||
|
courseId,
|
||||||
|
topicId,
|
||||||
|
category,
|
||||||
|
page: 'topics',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MemoryRouter initialEntries={[path]}>
|
||||||
|
<Route exact path={[Routes.POSTS.PATH, Routes.TOPICS.CATEGORY]}>
|
||||||
|
<TopicPostsView />
|
||||||
|
</Route>
|
||||||
|
<Route exact path={[Routes.TOPICS.ALL]}>
|
||||||
|
<PostActionsBar />
|
||||||
|
<TopicsView />
|
||||||
|
</Route>
|
||||||
|
<Route
|
||||||
|
render={({ location }) => {
|
||||||
|
lastLocation = location;
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</DiscussionContext.Provider>
|
||||||
|
</AppProvider>
|
||||||
|
</IntlProvider>,
|
||||||
|
);
|
||||||
|
container = wrapper.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('InContext Topic Posts View', () => {
|
||||||
|
let coursewareTopics;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: 3,
|
||||||
|
username: 'abc123',
|
||||||
|
administrator: true,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
store = initializeStore({
|
||||||
|
config: {
|
||||||
|
enableInContext: true,
|
||||||
|
provider: 'openedx',
|
||||||
|
hasModerationPrivileges: true,
|
||||||
|
blackouts: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Factory.resetAll();
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
lastLocation = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setupTopicsMockResponse() {
|
||||||
|
axiosMock.onGet(`${topicsApiUrl}${courseId}`)
|
||||||
|
.reply(200, (Factory.buildList('topic', 1, null, {
|
||||||
|
topicPrefix: 'noncourseware-topic',
|
||||||
|
enabledInContext: true,
|
||||||
|
topicNamePrefix: 'general-topic',
|
||||||
|
usageKey: '',
|
||||||
|
courseware: false,
|
||||||
|
discussionCount: 1,
|
||||||
|
questionCount: 1,
|
||||||
|
})
|
||||||
|
.concat(Factory.buildList('section', 2, null, { topicPrefix: 'courseware' })))
|
||||||
|
.concat(Factory.buildList('archived-topics', 2, null)));
|
||||||
|
await executeThunk(fetchCourseTopicsV3(courseId), store.dispatch, store.getState);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
coursewareTopics = selectCoursewareTopics(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupPostsMockResponse(topicId, numOfResponses = 3) {
|
||||||
|
axiosMock.onGet(threadsApiUrl)
|
||||||
|
.reply(() => {
|
||||||
|
const threadAttrs = { previewBody: 'thread preview body' };
|
||||||
|
return [200, Factory.build('threadsResult', {}, {
|
||||||
|
topicId,
|
||||||
|
threadAttrs,
|
||||||
|
count: numOfResponses,
|
||||||
|
})];
|
||||||
|
});
|
||||||
|
await executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ parentId: 'noncourseware-topic-1', parentTitle: 'general-topic-1', topicType: 'NonCourseware' },
|
||||||
|
{ parentId: 'courseware-topic-1-v3-1', parentTitle: 'Introduction Introduction 1-1-1', topicType: 'Courseware' },
|
||||||
|
])('\'$topicType\' topic should have a required number of post lengths.', async ({ parentId, parentTitle }) => {
|
||||||
|
await setupTopicsMockResponse();
|
||||||
|
await setupPostsMockResponse(parentId, 3);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
renderComponent({ topicId: parentId });
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(async () => {
|
||||||
|
const posts = await container.querySelectorAll('.discussion-post');
|
||||||
|
const backButton = screen.getByLabelText('Back to topics list');
|
||||||
|
const parentHeader = await screen.findByText(parentTitle);
|
||||||
|
|
||||||
|
expect(lastLocation.pathname.endsWith(`/topics/${parentId}`)).toBeTruthy();
|
||||||
|
expect(posts).toHaveLength(3);
|
||||||
|
expect(backButton).toBeInTheDocument();
|
||||||
|
expect(parentHeader).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('A back button should redirect from list of posts to list of units.', async () => {
|
||||||
|
await setupTopicsMockResponse();
|
||||||
|
const subSection = coursewareTopics[0].children[0];
|
||||||
|
const unit = subSection.children[0];
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
setupPostsMockResponse(unit.id, 2);
|
||||||
|
renderComponent({ topicId: unit.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const backButton = await screen.getByLabelText('Back to topics list');
|
||||||
|
|
||||||
|
await act(async () => fireEvent.click(backButton));
|
||||||
|
await waitFor(async () => {
|
||||||
|
renderComponent({ category: subSection.id });
|
||||||
|
|
||||||
|
const subSectionList = await container.querySelector('.list-group');
|
||||||
|
const units = subSectionList.querySelectorAll('.discussion-topic');
|
||||||
|
const unitHeader = within(subSectionList).queryByText(unit.name);
|
||||||
|
|
||||||
|
expect(lastLocation.pathname.endsWith(`/category/${subSection.id}`)).toBeTruthy();
|
||||||
|
expect(unitHeader).toBeInTheDocument();
|
||||||
|
expect(units).toHaveLength(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('A back button should redirect from units to the parent/selected subsection.', async () => {
|
||||||
|
await setupTopicsMockResponse();
|
||||||
|
const subSection = coursewareTopics[0].children[0];
|
||||||
|
|
||||||
|
renderComponent({ category: subSection.id });
|
||||||
|
|
||||||
|
const backButton = await screen.getByLabelText('Back to topics list');
|
||||||
|
|
||||||
|
await act(async () => fireEvent.click(backButton));
|
||||||
|
await waitFor(async () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const sectionList = await container.querySelector('.list-group');
|
||||||
|
const subSections = sectionList.querySelectorAll('.discussion-topic-group');
|
||||||
|
const subSectionHeader = within(sectionList).queryByText(subSection.displayName);
|
||||||
|
|
||||||
|
expect(lastLocation.pathname.endsWith('/topics')).toBeTruthy();
|
||||||
|
expect(subSectionHeader).toBeInTheDocument();
|
||||||
|
expect(subSections).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ searchText: 'hello world', output: 'Showing 0 results for', resultCount: 0 },
|
||||||
|
{ searchText: 'introduction', output: 'Showing 8 results for', resultCount: 8 },
|
||||||
|
])('It should have a search bar with a clear button and \'$output\' results found text.',
|
||||||
|
async ({ searchText, output, resultCount }) => {
|
||||||
|
await setupTopicsMockResponse();
|
||||||
|
await renderComponent();
|
||||||
|
|
||||||
|
const searchField = await within(container).getByPlaceholderText('Search topics');
|
||||||
|
const searchButton = await within(container).getByTestId('search-icon');
|
||||||
|
fireEvent.change(searchField, { target: { value: searchText } });
|
||||||
|
|
||||||
|
await waitFor(async () => expect(searchField).toHaveValue(searchText));
|
||||||
|
await act(async () => fireEvent.click(searchButton));
|
||||||
|
await waitFor(async () => {
|
||||||
|
const clearButton = await within(container).queryByText('Clear results');
|
||||||
|
const searchMessage = within(container).queryByText(`${output} "${searchText}"`);
|
||||||
|
const units = container.querySelectorAll('.discussion-topic');
|
||||||
|
|
||||||
|
expect(searchMessage).toBeInTheDocument();
|
||||||
|
expect(clearButton).toBeInTheDocument();
|
||||||
|
expect(units).toHaveLength(resultCount);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('When click on the clear button it should move to main topics pages.', async () => {
|
||||||
|
await setupTopicsMockResponse();
|
||||||
|
await renderComponent();
|
||||||
|
|
||||||
|
const searchText = 'hello world';
|
||||||
|
const searchField = await within(container).getByPlaceholderText('Search topics');
|
||||||
|
const searchButton = await within(container).getByTestId('search-icon');
|
||||||
|
|
||||||
|
fireEvent.change(searchField, { target: { value: searchText } });
|
||||||
|
|
||||||
|
await waitFor(async () => expect(searchField).toHaveValue(searchText));
|
||||||
|
await act(async () => fireEvent.click(searchButton));
|
||||||
|
await waitFor(async () => {
|
||||||
|
const clearButton = await within(container).queryByText('Clear results');
|
||||||
|
|
||||||
|
await act(async () => fireEvent.click(clearButton));
|
||||||
|
await waitFor(async () => {
|
||||||
|
const coursewareTopicList = await container.querySelectorAll('.discussion-topic-group');
|
||||||
|
|
||||||
|
expect(coursewareTopicList).toHaveLength(3);
|
||||||
|
expect(within(container).queryByText('Clear results')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -50,6 +50,7 @@ function TopicSearchBar({ intl }) {
|
|||||||
<Icon
|
<Icon
|
||||||
src={SearchIcon}
|
src={SearchIcon}
|
||||||
onClick={() => onSubmit(searchValue)}
|
onClick={() => onSubmit(searchValue)}
|
||||||
|
data-testid="search-icon"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</SearchField.Advanced>
|
</SearchField.Advanced>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ function Comment({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<EndorsedAlertBanner postType={postType} content={comment} />
|
<EndorsedAlertBanner postType={postType} content={comment} />
|
||||||
<div className="d-flex flex-column post-card-comment px-4 pt-3.5 pb-10px" aria-level={5}>
|
<div className="d-flex flex-column post-card-comment px-4 pt-3.5 pb-10px" tabIndex="0">
|
||||||
<HoverCard
|
<HoverCard
|
||||||
commentOrPost={comment}
|
commentOrPost={comment}
|
||||||
actionHandlers={actionHandlers}
|
actionHandlers={actionHandlers}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useEffect } from 'react';
|
|||||||
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
|
|
||||||
import { EndorsementStatus } from '../../../data/constants';
|
import { EndorsementStatus } from '../../../data/constants';
|
||||||
import { useDispatchWithState } from '../../../data/hooks';
|
import { useDispatchWithState } from '../../../data/hooks';
|
||||||
import { selectThread } from '../../posts/data/selectors';
|
import { selectThread } from '../../posts/data/selectors';
|
||||||
@@ -11,6 +13,16 @@ import {
|
|||||||
} from './selectors';
|
} from './selectors';
|
||||||
import { fetchThreadComments } from './thunks';
|
import { fetchThreadComments } from './thunks';
|
||||||
|
|
||||||
|
function trackLoadMoreEvent(postId, params) {
|
||||||
|
sendTrackEvent(
|
||||||
|
'edx.forum.responses.loadMore',
|
||||||
|
{
|
||||||
|
postId,
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function usePost(postId) {
|
export function usePost(postId) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const thread = useSelector(selectThread(postId));
|
const thread = useSelector(selectThread(postId));
|
||||||
@@ -31,11 +43,15 @@ export function usePostComments(postId, endorsed = null) {
|
|||||||
const hasMorePages = useSelector(selectThreadHasMorePages(postId, endorsed));
|
const hasMorePages = useSelector(selectThreadHasMorePages(postId, endorsed));
|
||||||
const currentPage = useSelector(selectThreadCurrentPage(postId, endorsed));
|
const currentPage = useSelector(selectThreadCurrentPage(postId, endorsed));
|
||||||
|
|
||||||
const handleLoadMoreResponses = async () => dispatch(fetchThreadComments(postId, {
|
const handleLoadMoreResponses = async () => {
|
||||||
endorsed,
|
const params = {
|
||||||
page: currentPage + 1,
|
endorsed,
|
||||||
reverseOrder,
|
page: currentPage + 1,
|
||||||
}));
|
reverseOrder,
|
||||||
|
};
|
||||||
|
await dispatch(fetchThreadComments(postId, params));
|
||||||
|
trackLoadMoreEvent(postId, params);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchThreadComments(postId, {
|
dispatch(fetchThreadComments(postId, {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ function PostEditor({
|
|||||||
const nonCoursewareIds = useSelector(enableInContext ? inContextCoursewareIds : selectNonCoursewareIds);
|
const nonCoursewareIds = useSelector(enableInContext ? inContextCoursewareIds : selectNonCoursewareIds);
|
||||||
const coursewareTopics = useSelector(enableInContext ? inContextCourseware : selectCoursewareTopics);
|
const coursewareTopics = useSelector(enableInContext ? inContextCourseware : selectCoursewareTopics);
|
||||||
const cohorts = useSelector(selectCourseCohorts);
|
const cohorts = useSelector(selectCourseCohorts);
|
||||||
const post = useSelector(selectThread(postId));
|
const post = useSelector(editExisting ? selectThread(postId) : () => ({}));
|
||||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||||
const settings = useSelector(selectDivisionSettings);
|
const settings = useSelector(selectDivisionSettings);
|
||||||
|
|||||||
@@ -141,6 +141,16 @@ describe('PostEditor', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
test('selectThread is not called while creating a new post', async () => {
|
||||||
|
const mockSelectThread = jest.fn();
|
||||||
|
jest.mock('../data/selectors', () => ({
|
||||||
|
selectThread: mockSelectThread,
|
||||||
|
}));
|
||||||
|
await renderComponent();
|
||||||
|
expect(mockSelectThread)
|
||||||
|
.not
|
||||||
|
.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('cohorting', () => {
|
describe('cohorting', () => {
|
||||||
|
|||||||
@@ -90,8 +90,9 @@ function Post({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="d-flex flex-column w-100 mw-100 post-card-comment"
|
className="d-flex flex-column w-100 mw-100 post-card-comment"
|
||||||
aria-level={5}
|
|
||||||
data-testid={`post-${post.id}`}
|
data-testid={`post-${post.id}`}
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||||
|
tabIndex="0"
|
||||||
>
|
>
|
||||||
<Confirmation
|
<Confirmation
|
||||||
isOpen={isDeleting}
|
isOpen={isDeleting}
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ header {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.on-focus:focus-visible {
|
.on-focus:focus-within {
|
||||||
outline: 2px solid black;
|
outline: 2px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,6 +442,8 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.post-card-comment {
|
.post-card-comment {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
&:not(:hover),
|
&:not(:hover),
|
||||||
&:not(:focus) {
|
&:not(:focus) {
|
||||||
.hover-card {
|
.hover-card {
|
||||||
@@ -450,7 +452,7 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus-within {
|
||||||
.hover-card {
|
.hover-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user