diff --git a/.eslintrc b/.eslintrc index 531acf5e568d6151e7015f7a87328681ff381cc0..167c168efb7b04a6b7ee0581e4956c7a14a7bd25 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,13 +2,16 @@ "env": { "jest/globals": true }, - "extends": ["airbnb","react-app"], + "extends": ["airbnb","react-app", "plugin:jest-dom/recommended", "plugin:testing-library/react"], "globals": { "page": true, "document": true }, - "plugins": ["jest"], + "plugins": ["jest", "jest-dom", "react", "react-hooks", "testing-library"], "rules": { + "import/no-unresolved": [ + 2, { "ignore": ["test-utils"] } + ], "import/prefer-default-export": "off", "no-console": "off", "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], @@ -40,6 +43,14 @@ "ignoreRegExpLiterals": true }], "react/jsx-uses-react": "off", - "react/react-in-jsx-scope": "off" + "react/react-in-jsx-scope": "off", + "react-hooks/exhaustive-deps": "error", + "testing-library/render-result-naming-convention": "off", + "testing-library/no-render-in-setup": [ + "error", + { + "allowTestingFrameworkSetupHook": "beforeEach" + } + ] } } diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index e7a7aa0a78c123022ec6627e5c4a33a8e5dc80cc..bc23b2fac744bc5b946bcaf715b8910393e857ea 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -7,19 +7,19 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] + branches: [ master, mui5 ] jobs: build: runs-on: ubuntu-latest strategy: matrix: - node-version: [16.x, 18.x, 19.x] + node-version: [18.x, 20.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000000000000000000000000000000000..0ad0fc419a16bf4e29328c761b079ecd5d1ba1e9 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Mirador Code of Conduct + +The [IIIF Code of Conduct](https://iiif.io/event/conduct/) guides our behavior as members of the IIIF Community in any forum, including remote and in-person meetings and events, email lists, Slack, GitHub, social media, and private correspondence related to IIIF and the Mirador project. IIIF is an inclusive and safe community, committed to openness and transparency in all interactions and activities. To reflect these commitments, we agree to operate according to the following standards. + +> **Note:** Please visit the [IIIF Code of Conduct](https://iiif.io/event/conduct/) page for the complete and current standards, which includes additional details regarding the process for evaluating Code of Conduct reports, the appeals process, and the IIIF Code of Conduct Team involved in these activities. + + + - [Community Ideals](#community-ideals) + - [Inclusivity](#inclusivity) + - [Collaboration](#collaboration) + - [Respect](#respect) + - [Greater Good](#greater-good) + - [Unacceptable Behaviors](#unacceptable-behaviors) + - [Reporting Harassment](#reporting-harassment) + - [Project Maintainer Enforcement Responsibilities](#project-maintainer-enforcement-responsibilities) + - [Acknowledgements](#acknowledgements) + + +## Community Ideals + +### Inclusivity +We believe everyone is a valuable contributor to the community regardless of physical or intellectual ability, gender, sexual orientation, physical appearance, race, religion, nationality, language, employment source or status, financial ability, and any other facets of community dynamics not mentioned here. While the community primarily communicates in English at this point, we recognize that it is not the native language for many in the community and strive to express ourselves simply and clearly. We provide opportunities for participation despite challenges of ability, time, geography, finances, etc. + +### Collaboration + +We collaborate to share the community’s workload and to ensure a variety of needs and voices are accounted for. We do our work as transparently as possible, establishing guidelines for contributing and sharing current and historical community information openly, and welcome new collaborators with enthusiasm. + +### Respect + +The IIIF community is respectful of and open to differing opinions, viewpoints, experiences, and backgrounds. When we make a mistake we accept responsibility, apologize, and learn from the experience. We give and gracefully accept constructive feedback. + +### Greater Good + +We do work that benefits the community. We notice, call attention to, and help resolve potential problems. Community members should model good behavior and feel empowered to be [active bystanders](https://www.diglib.org/active-bystander-orientation/) to call out improper behavior and step in to help resolve issues without waiting to escalate to a higher power. + + +## Unacceptable Behaviors + +Participants asked by anyone to stop any harassing behavior must comply immediately. Participants who violate these guidelines by behaving in the following ways will be notified and asked to change their behavior. + +- Physical or verbal harassment or threats directed against another person or group of people +- Offensive verbal comments, insults, or jokes related to physical or intellectual ability, gender, sexual orientation, disability, physical appearance, body size, race, religion, etc. +- Sexual harassment, including inappropriate or unwanted physical contact, use of sexualized language or imagery, and sexual attention or advances of any kind +- Intentional provocation, insulting or derogatory comments, and personal or political attacks +- Publishing or threatening to publish others’ private information without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting +- Advocating for or encouraging any of the above + +## Reporting Harassment + +If you believe anyone is in physical danger, please notify appropriate law enforcement first. + +If you are subject to or witness harassment, please contact the Code of Conduct committee at [iiif-conduct@googlegroups.com](mailto:iiif-conduct@googlegroups.com), or individually at in-person events where committee members or event-specific contacts will be identified by badges and introductions at the beginning of the event. + +**Written or verbal Code of Conduct reports should include the following information:** +- Your contact information +- Names (real, nicknames, or pseudonyms) of individuals involved and witnesses +- When and where the incident occurred (please be as specific as possible) +- Your account of what occurred. If there is a record available (e.g. a mailing list archive or messages in a Slack channel) please include a link or screenshot. +- If you believe this incident is or may be ongoing + +You will receive an email or verbal confirmation that your report has been received. Within one week you will receive a resolution and course of action or explanation of why the situation is unresolved and continues to be evaluated. + +## Project Maintainer Enforcement Responsibilities + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct. + +## Acknowledgements + +The IIIF Code of Conduct is modeled on examples from [Contributor Covenant](https://www.contributor-covenant.org/version/2/0/code_of_conduct/), [Django](https://www.djangoproject.com/conduct/), [the Digital Library Federation (DLF)](https://www.diglib.org/about/code-of-conduct/), [Islandora](https://www.islandora.ca/code-of-conduct), [Samvera](https://wiki.duraspace.org/display/samvera/Code+of+Conduct), and [DuraSpace](https://duraspace.org/about/policies/code-of-conduct/#:~:text=All%20communication%20will%20be%20treated,exclusionary%20behavior%20in%20any%20form.). + +Project maintainer enforcement responsibilities are modeled on the [Contributor Covenant](https://www.contributor-covenant.org/version/2/0/code_of_conduct/). + +The IIIF Code of Conduct is licensed under a [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). diff --git a/LICENSE b/LICENSE index dcead47785cefd97b62fbad588bd683889749964..c44985806a31dd1e76e11b194ec17a666f3302bd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,203 @@ -This project is dual-licensed under the Apache License 2.0 and the MIT license. +Copyright 2023 The Board of Trustees of the Leland Stanford Junior University -Copyright 2021 Digital Humanities Initiative, Center for Evolving Humanities, Graduate School of Humanities and Sociology, The University of Tokyo -Copyright 2021 International Institute for Digital Humanities -Copyright 2021 Research Institute for Languages and Cultures of Asia and Africa, Tokyo University of Foreign Studies -Copyright 2021 FLX Style + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Includes content from Mirador licensed under the Apache License 2.0. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Copyright 2020 The Board of Trustees of the Leland Stanford Junior University + 1. Definitions. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - http://www.apache.org/licenses/LICENSE-2.0 + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/__tests__/fixtures/version-3/video_with_annotation_captions.json b/__tests__/fixtures/version-3/video_with_annotation_captions.json new file mode 100644 index 0000000000000000000000000000000000000000..b09a3bd4f1e5f709e0a641147f8ab40093181a12 --- /dev/null +++ b/__tests__/fixtures/version-3/video_with_annotation_captions.json @@ -0,0 +1,70 @@ +{ + "@context": "http://iiif.io/api/presentation/3/context.json", + "id": "https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/manifest.json", + "type": "Manifest", + "label": { + "en": [ + "Lunchroom Manners" + ] + }, + "items": [ + { + "id": "https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/canvas", + "type": "Canvas", + "height": 360, + "width": 480, + "duration": 572.034, + "items": [ + { + "id": "https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/canvas/page", + "type": "AnnotationPage", + "items": [ + { + "id": "https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/canvas/page/annotation1", + "type": "Annotation", + "motivation": "painting", + "body": [ + { + "id": "https://fixtures.iiif.io/video/indiana/lunchroom_manners/high/lunchroom_manners_1024kb.mp4", + "type": "Video", + "height": 360, + "width": 480, + "duration": 572.034, + "format": "video/mp4" + } + ], + "target": "https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/canvas" + } + ] + } + ], + "annotations": [ + { + "id": "https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/canvas/page2", + "type": "AnnotationPage", + "items": [ + { + "id": "https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/canvas/page2/a1", + "type": "Annotation", + "motivation": "supplementing", + "body": [ + { + "id": "https://fixtures.iiif.io/video/indiana/lunchroom_manners/lunchroom_manners.vtt", + "type": "Text", + "format": "text/vtt", + "label": { + "en": [ + "Captions in WebVTT format" + ] + }, + "language": "en" + } + ], + "target": "https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/canvas" + } + ] + } + ] + } + ] +} diff --git a/__tests__/fixtures/version-3/with_a_provider.json b/__tests__/fixtures/version-3/with_a_provider.json index 2a0fd4d90c97b2c8e83c37c066215c85550a975b..1c27a14252215e7b3359e7f9900892cdcebfa9cc 100644 --- a/__tests__/fixtures/version-3/with_a_provider.json +++ b/__tests__/fixtures/version-3/with_a_provider.json @@ -10,18 +10,18 @@ "id": "https://example.org/about", "type": "Agent", "label": {"en": ["Example Organization"]}, - "homepage": { + "homepage": [{ "id": "https://example.org/", "type": "Text", "label": {"en": ["Example Organization Homepage"]}, "format": "text/html" - }, - "logo": { + }], + "logo": [{ "id": "https://example.org/images/logo.png", "type": "Image", "height": 100, "width": 120 - }, + }], "seeAlso": [ { "id": "https://data.example.org/about/us.jsonld", diff --git a/__tests__/integration/mirador/plugins/state.test.js b/__tests__/integration/mirador/plugins/state.test.js index 15e821ce441fbc9fc9cb3e5b1638ba894c326858..4631ecef124511696e1d1a0ee8aaceb8dbcd56aa 100644 --- a/__tests__/integration/mirador/plugins/state.test.js +++ b/__tests__/integration/mirador/plugins/state.test.js @@ -9,7 +9,7 @@ describe('how plugins relate to state', () => { it('plugin can read from state', async () => { const text = 'Plugin:https://iiif.harvardartmuseums.org/manifests/object/299843'; - await expect(page).toMatch(text); + await expect(page).toMatchTextContent(text); }); it('plugin reducers should be included to state', async () => { diff --git a/__tests__/integration/mirador/thumbnail-navigation.test.js b/__tests__/integration/mirador/thumbnail-navigation.test.js index 35de953ccaa3b13e04ec8ab4fb17bc7a9faf381e..82502cbd53e644f1417557d7fa56bb5f80f704f5 100644 --- a/__tests__/integration/mirador/thumbnail-navigation.test.js +++ b/__tests__/integration/mirador/thumbnail-navigation.test.js @@ -6,7 +6,8 @@ describe('Thumbnail navigation', () => { await expect(page).toMatchElement('.mirador-window', { polling: 'mutation', timeout: 5000 }); }); - it('navigates a manifest using thumbnail navigation', async () => { + // TODO: pick a new url; this Harvard one is 404 + xit('navigates a manifest using thumbnail navigation', async () => { await expect(page).toMatchElement('.mirador-thumb-navigation'); let windows = await page.evaluate(() => ( miradorInstance.store.getState().windows @@ -20,7 +21,7 @@ describe('Thumbnail navigation', () => { )); expect(Object.values(windows)[0].canvasId).toBe('https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-18737483'); // canvas @ index 1 }); - it('displays on right side', async () => { + xit('displays on right side', async () => { await expect(page).toMatchElement('.mirador-thumb-navigation'); await expect(page).toMatchElement('.mirador-companion-area-far-bottom .mirador-thumb-navigation'); const windowId = await page.evaluate(() => { diff --git a/__tests__/src/actions/companionWindow.test.js b/__tests__/src/actions/companionWindow.test.js index a90108559459c8050fa30678de5398e7b6fb2fa6..3d779c23f45bd661b38a38a91d72f12dec4a6b1a 100644 --- a/__tests__/src/actions/companionWindow.test.js +++ b/__tests__/src/actions/companionWindow.test.js @@ -2,7 +2,7 @@ import * as actions from '../../../src/state/actions'; import ActionTypes from '../../../src/state/actions/action-types'; jest.mock('../../../src/state/selectors', () => ({ - getManuallyExpandedNodeIds: (state, args, expanded) => (expanded ? ['openVisible', 'open'] : ['closedVisible', 'closed']), + ...jest.requireActual('../../../src/state/selectors'), getVisibleNodeIds: (state, args) => ['openVisible', 'closedVisible', 'visible'], })); @@ -53,13 +53,73 @@ describe('companionWindow actions', () => { }); }); + describe('expandNodes', () => { + let mockDispatch; + let mockGetState; + let mockState; + + beforeEach(() => { + mockState = { + companionWindows: { + cw1: {}, + }, + }; + mockDispatch = jest.fn(() => ({})); + mockGetState = jest.fn(() => mockState); + }); + + it('marks the provided nodes as expanded', () => { + const thunk = actions.expandNodes('window1', 'cw1', ['a', 'b', 'c']); + thunk(mockDispatch, mockGetState); + + const action = mockDispatch.mock.calls[0][0]; + expect(action.id).toBe('cw1'); + expect(action.windowId).toBe('window1'); + expect(action.type).toBe(ActionTypes.TOGGLE_TOC_NODE); + expect(action.payload).toMatchObject({ a: { expanded: true }, b: { expanded: true }, c: { expanded: true } }); + }); + + it('marks currently expanded nodes as collapsed', () => { + mockState.companionWindows = { + cw1: { + tocNodes: { + a: { expanded: true }, + b: { expanded: true }, + c: { expanded: true }, + }, + }, + }; + + const thunk = actions.expandNodes('window1', 'cw1', ['a']); + thunk(mockDispatch, mockGetState); + + const action = mockDispatch.mock.calls[0][0]; + expect(action.id).toBe('cw1'); + expect(action.windowId).toBe('window1'); + expect(action.type).toBe(ActionTypes.TOGGLE_TOC_NODE); + expect(action.payload).toMatchObject({ a: { expanded: true }, b: { expanded: false }, c: { expanded: false } }); + }); + }); + describe('toggleNode', () => { let mockDispatch; let mockGetState; + let mockState; beforeEach(() => { + mockState = { + companionWindows: { + cw1: { + tocNodes: { + closedVisible: { expanded: false }, + open: { expanded: true }, + openVisible: { expanded: true }, + }, + }, + }, + }; mockDispatch = jest.fn(() => ({})); - mockGetState = jest.fn(() => ({})); + mockGetState = jest.fn(() => mockState); }); it('returns a collapsing action for visible nodes that are not present in the state', () => { diff --git a/__tests__/src/components/AccessTokenSender.test.js b/__tests__/src/components/AccessTokenSender.test.js index 0281b4efb78581ee59f760a49d9e775c05b9d799..251896c4b9c7dd1d5c47cef6a898c1bd743e10d3 100644 --- a/__tests__/src/components/AccessTokenSender.test.js +++ b/__tests__/src/components/AccessTokenSender.test.js @@ -1,11 +1,11 @@ -import { shallow } from 'enzyme'; +import { render } from 'test-utils'; import { AccessTokenSender } from '../../../src/components/AccessTokenSender'; /** * Helper function to create a shallow wrapper around ErrorDialog */ function createWrapper(props) { - return shallow( + return render( <AccessTokenSender t={key => key} handleAccessTokenMessage={() => {}} @@ -15,36 +15,28 @@ function createWrapper(props) { } describe('AccessTokenSender', () => { - let wrapper; - it('renders nothing if there is no url', () => { - wrapper = createWrapper({}); - expect(wrapper.isEmptyRender()).toBe(true); + const { container } = createWrapper({}); + expect(container).toBeEmptyDOMElement(); }); it('renders properly', () => { - Object.defineProperty(window, 'origin', { - value: 'http://localhost', - writable: true, - }); - wrapper = createWrapper({ url: 'http://example.com' }); - expect(wrapper.find('IComCom').length).toBe(1); - expect(wrapper.find('IComCom').props().attributes.src).toBe('http://example.com?origin=http://localhost&messageId=http://example.com'); + const { container } = createWrapper({ url: 'http://example.com' }); + + expect(container.querySelector('iframe')).toHaveAttribute('src', 'http://example.com?origin=http://localhost&messageId=http://example.com'); // eslint-disable-line testing-library/no-node-access, testing-library/no-container }); it('triggers an action when the iframe sends a message', () => { const handleAccessTokenMessage = jest.fn(); - wrapper = createWrapper({ handleAccessTokenMessage, url: 'http://example.com' }); - expect(wrapper.find('IComCom').props().handleReceiveMessage).toEqual(wrapper.instance().onReceiveAccessTokenMessage); - - wrapper.instance().onReceiveAccessTokenMessage({ data: { messageId: 'http://example.com' } }); + createWrapper({ handleAccessTokenMessage, url: 'http://example.com' }); + window.dispatchEvent(new MessageEvent('message', { data: { messageId: 'http://example.com' } })); expect(handleAccessTokenMessage).toHaveBeenCalledWith({ messageId: 'http://example.com' }); }); it('ignores iframe messages with the wrong messageId', () => { const handleAccessTokenMessage = jest.fn(); - wrapper = createWrapper({ handleAccessTokenMessage, url: 'http://example.com' }); - wrapper.instance().onReceiveAccessTokenMessage({ data: { messageId: 'http://example.com/123' } }); + createWrapper({ handleAccessTokenMessage, url: 'http://example.com' }); + window.dispatchEvent(new MessageEvent('message', { data: { messageId: 'http://example.com/123' } })); expect(handleAccessTokenMessage).not.toHaveBeenCalled(); }); }); diff --git a/__tests__/src/components/AnnotationSettings.test.js b/__tests__/src/components/AnnotationSettings.test.js index d15a537ec7c8c40df63284723da1d255b4131211..84e04d7b86efd9bb92db0ffcb91d093325c32e12 100644 --- a/__tests__/src/components/AnnotationSettings.test.js +++ b/__tests__/src/components/AnnotationSettings.test.js @@ -1,10 +1,10 @@ -import { shallow } from 'enzyme'; -import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { AnnotationSettings } from '../../../src/components/AnnotationSettings'; /** */ function createWrapper(props) { - return shallow( + return render( <AnnotationSettings displayAll={false} displayAllDisabled={false} @@ -17,17 +17,18 @@ function createWrapper(props) { } describe('AnnotationSettings', () => { - let wrapper; const toggleAnnotationDisplayMock = jest.fn(); it('renders a MiradorMenuButton', () => { - wrapper = createWrapper(); - expect(wrapper.find(MiradorMenuButton).length).toBe(1); + createWrapper(); + expect(screen.getByRole('button')).toBeInTheDocument(); }); - it('calls the toggleAnnotationDisplay prop function on click', () => { - wrapper = createWrapper({ toggleAnnotationDisplay: toggleAnnotationDisplayMock }); - wrapper.find(MiradorMenuButton).simulate('click'); + it('calls the toggleAnnotationDisplay prop function on click', async () => { + const user = userEvent.setup(); + + createWrapper({ toggleAnnotationDisplay: toggleAnnotationDisplayMock }); + await user.click(screen.getByRole('button')); expect(toggleAnnotationDisplayMock).toHaveBeenCalledTimes(1); }); diff --git a/__tests__/src/components/AnnotationsOverlay.test.js b/__tests__/src/components/AnnotationsOverlay.test.js index 4766b19e921ef077fec534574be27c5faf45985a..fe3d1165407a3d70eabf790410ae480f45018f65 100644 --- a/__tests__/src/components/AnnotationsOverlay.test.js +++ b/__tests__/src/components/AnnotationsOverlay.test.js @@ -1,4 +1,5 @@ -import { shallow } from 'enzyme'; +import { cloneElement } from 'react'; +import { render, screen } from 'test-utils'; import OpenSeadragon from 'openseadragon'; import { Utils } from 'manifesto.js'; import { AnnotationsOverlay } from '../../../src/components/AnnotationsOverlay'; @@ -9,34 +10,33 @@ import fixture from '../../fixtures/version-2/019.json'; const canvases = Utils.parseManifest(fixture).getSequences()[0].getCanvases(); -jest.mock('react-dom'); -jest.mock('openseadragon'); jest.mock('../../../src/lib/OpenSeadragonCanvasOverlay'); +/** */ +const createWrapper = (props) => { + render(<canvas data-testid="viewer" />); + const viewer = new OpenSeadragon({ element: screen.getByTestId('viewer') }); + const component = ( + <AnnotationsOverlay + annotations={[]} + viewer={viewer} + classes={{}} + searchAnnotations={[]} + windowId="base" + config={{}} + updateViewport={jest.fn()} + t={k => k} + canvasWorld={new CanvasWorld(canvases)} + {...props} + /> + ); + + return { component, viewer, ...render(component) }; +}; + describe('AnnotationsOverlay', () => { - let wrapper; - let viewer; - let updateViewport; beforeEach(() => { - OpenSeadragon.mockClear(); OpenSeadragonCanvasOverlay.mockClear(); - - updateViewport = jest.fn(); - viewer = { addHandler: () => {}, forceRedraw: () => {} }; - - wrapper = shallow( - <AnnotationsOverlay - annotations={[]} - viewer={viewer} - classes={{}} - searchAnnotations={[]} - windowId="base" - config={{}} - updateViewport={updateViewport} - t={k => k} - canvasWorld={new CanvasWorld(canvases)} - />, - ); }); describe('annotationsMatch', () => { @@ -83,103 +83,94 @@ describe('AnnotationsOverlay', () => { describe('componentDidUpdate', () => { it('sets up a OpenSeadragonCanvasOverlay', () => { - wrapper.instance().componentDidUpdate({}); + const { component, rerender } = createWrapper(); + + rerender(cloneElement(component, { classes: { whatever: 'value' } })); expect(OpenSeadragonCanvasOverlay).toHaveBeenCalledTimes(1); }); it('sets up a listener on update-viewport', () => { - wrapper.instance().osdCanvasOverlay = null; - const addHandler = jest.fn(); - viewer.addHandler = addHandler; - wrapper.instance().componentDidUpdate({}); - expect(addHandler).toHaveBeenCalledWith('update-viewport', expect.anything()); + const { component, rerender, viewer } = createWrapper({ viewer: null }); + const mockAddHandler = jest.spyOn(viewer, 'addHandler'); + + rerender(cloneElement(component, { viewer })); + expect(mockAddHandler).toHaveBeenCalledWith('update-viewport', expect.anything()); }); it('sets up canvasUpdate to add annotations to the canvas and forces a redraw', () => { const clear = jest.fn(); const resize = jest.fn(); const canvasUpdate = jest.fn(); - const forceRedraw = jest.fn(); - wrapper.instance().osdCanvasOverlay = { + OpenSeadragonCanvasOverlay.mockImplementation(() => ({ canvasUpdate, clear, resize, - }; + })); - viewer.forceRedraw = forceRedraw; + const { component, rerender, viewer } = createWrapper({ viewer: null }); - wrapper.setProps( - { - annotations: [ - new AnnotationList( - { '@id': 'foo', resources: [{ foo: 'bar' }] }, - ), - ], - }, - ); - wrapper.setProps( + const forceRedraw = jest.spyOn(viewer, 'forceRedraw'); + + rerender(cloneElement( + component, { annotations: [ new AnnotationList( { '@id': 'foo', resources: [{ foo: 'bar' }] }, ), ], + viewer, }, - ); - wrapper.setProps( - { - annotations: [ - new AnnotationList( - { '@id': 'bar', resources: [{ foo: 'bar' }] }, - ), - ], - }, - ); - wrapper.instance().updateCanvas(); - expect(clear).toHaveBeenCalledTimes(1); - expect(resize).toHaveBeenCalledTimes(1); - expect(canvasUpdate).toHaveBeenCalledTimes(1); - expect(forceRedraw).toHaveBeenCalled(); - }); - }); + )); - describe('onUpdateViewport', () => { - it('fires updateCanvas', () => { - const updateCanvas = jest.fn(); - wrapper.instance().updateCanvas = updateCanvas; - wrapper.instance().onUpdateViewport(); - expect(updateCanvas).toHaveBeenCalledTimes(1); + // OSD ordinarily would fire this event: + viewer.raiseEvent('update-viewport'); + + expect(clear).toHaveBeenCalled(); + expect(resize).toHaveBeenCalled(); + expect(canvasUpdate).toHaveBeenCalled(); + expect(forceRedraw).toHaveBeenCalled(); }); }); describe('annotationsToContext', () => { it('converts the annotations to canvas and checks that the canvas is displayed', () => { const strokeRect = jest.fn(); - wrapper.instance().osdCanvasOverlay = { - context2d: { - restore: () => {}, - save: () => {}, - strokeRect, - }, - }; - viewer.viewport = { - getMaxZoom: () => (1), - getZoom: () => (0.05), + const context2d = { + restore: () => { }, + save: () => { }, + strokeRect, }; - const annotations = [ - new AnnotationList( - { '@id': 'foo', resources: [{ on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=10,10,100,200' }] }, - ), - ]; + OpenSeadragonCanvasOverlay.mockImplementation(() => ({ + canvasUpdate: (f) => f(), + clear: jest.fn(), + context2d, + resize: jest.fn(), + })); const palette = { default: { strokeStyle: 'yellow' }, }; + const { component, rerender, viewer } = createWrapper({ palette: { annotations: palette }, viewer: null }); + + jest.spyOn(viewer.viewport, 'getMaxZoom').mockImplementation(() => (1)); + jest.spyOn(viewer.viewport, 'getZoom').mockImplementation(() => (0.05)); + + rerender(cloneElement(component, { + annotations: [ + new AnnotationList( + { '@id': 'foo', resources: [{ on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=10,10,100,200' }] }, + ), + ], + viewer, + })); - wrapper.instance().annotationsToContext(annotations, palette); - const context = wrapper.instance().osdCanvasOverlay.context2d; + // OSD ordinarily would fire this event: + viewer.raiseEvent('update-viewport'); + + const context = context2d; expect(context.strokeStyle).toEqual('yellow'); expect(context.lineWidth).toEqual(20); expect(strokeRect).toHaveBeenCalledWith(10, 10, 100, 200); @@ -190,28 +181,26 @@ describe('AnnotationsOverlay', () => { it('triggers a selectAnnotation for the clicked-on annotation', () => { const selectAnnotation = jest.fn(); - wrapper.setProps( - { - annotations: [ - new AnnotationList( - { - '@id': 'foo', - resources: [{ - '@id': 'http://example.org/identifier/annotation/anno-line', - '@type': 'oa:Annotation', - motivation: 'sc:painting', - on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=100,100,250,20', - }], - }, - ), - ], - selectAnnotation, - }, - ); + const { viewer } = createWrapper({ + annotations: [ + new AnnotationList( + { + '@id': 'foo', + resources: [{ + '@id': 'http://example.org/identifier/annotation/anno-line', + '@type': 'oa:Annotation', + motivation: 'sc:painting', + on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=100,100,250,20', + }], + }, + ), + ], + selectAnnotation, + }); - wrapper.instance().onCanvasClick({ - eventSource: { viewport: { pointFromPixel: point => ({ x: 101, y: 101 }) } }, - position: { x: 0, y: 0 }, + viewer.raiseEvent('canvas-click', { + eventSource: { viewport: viewer.viewport }, + position: new OpenSeadragon.Point(101, 101), }); expect(selectAnnotation).toHaveBeenCalledWith('base', 'http://example.org/identifier/annotation/anno-line'); @@ -220,29 +209,27 @@ describe('AnnotationsOverlay', () => { it('triggers a deselectAnnotation for an already-selected annotation', () => { const deselectAnnotation = jest.fn(); - wrapper.setProps( - { - annotations: [ - new AnnotationList( - { - '@id': 'foo', - resources: [{ - '@id': 'http://example.org/identifier/annotation/anno-line', - '@type': 'oa:Annotation', - motivation: 'sc:painting', - on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=100,100,250,20', - }], - }, - ), - ], - deselectAnnotation, - selectedAnnotationId: 'http://example.org/identifier/annotation/anno-line', - }, - ); + const { viewer } = createWrapper({ + annotations: [ + new AnnotationList( + { + '@id': 'foo', + resources: [{ + '@id': 'http://example.org/identifier/annotation/anno-line', + '@type': 'oa:Annotation', + motivation: 'sc:painting', + on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=100,100,250,20', + }], + }, + ), + ], + deselectAnnotation, + selectedAnnotationId: 'http://example.org/identifier/annotation/anno-line', + }); - wrapper.instance().onCanvasClick({ - eventSource: { viewport: { pointFromPixel: point => ({ x: 101, y: 101 }) } }, - position: { x: 0, y: 0 }, + viewer.raiseEvent('canvas-click', { + eventSource: { viewport: viewer.viewport }, + position: new OpenSeadragon.Point(101, 101), }); expect(deselectAnnotation).toHaveBeenCalledWith('base', 'http://example.org/identifier/annotation/anno-line'); @@ -251,38 +238,36 @@ describe('AnnotationsOverlay', () => { it('selects the closest annotation', () => { const selectAnnotation = jest.fn(); - wrapper.setProps( - { - annotations: [ - new AnnotationList( - { - '@id': 'foo', - resources: [{ - '@id': 'http://example.org/identifier/annotation/anno-line', - '@type': 'oa:Annotation', - motivation: 'sc:painting', - on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=100,100,250,20', - }, { - '@id': 'http://example.org/identifier/annotation/larger-box', - '@type': 'oa:Annotation', - motivation: 'sc:painting', - on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=0,0,250,250', - }, { - '@id': 'http://example.org/identifier/annotation/on-another-canvas', - '@type': 'oa:Annotation', - motivation: 'sc:painting', - on: 'http://iiif.io/some-other-canvas#xywh=101,101,3,3', - }], - }, - ), - ], - selectAnnotation, - }, - ); + const { viewer } = createWrapper({ + annotations: [ + new AnnotationList( + { + '@id': 'foo', + resources: [{ + '@id': 'http://example.org/identifier/annotation/anno-line', + '@type': 'oa:Annotation', + motivation: 'sc:painting', + on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=100,100,250,20', + }, { + '@id': 'http://example.org/identifier/annotation/larger-box', + '@type': 'oa:Annotation', + motivation: 'sc:painting', + on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=0,0,250,250', + }, { + '@id': 'http://example.org/identifier/annotation/on-another-canvas', + '@type': 'oa:Annotation', + motivation: 'sc:painting', + on: 'http://iiif.io/some-other-canvas#xywh=101,101,3,3', + }], + }, + ), + ], + selectAnnotation, + }); - wrapper.instance().onCanvasClick({ - eventSource: { viewport: { pointFromPixel: point => ({ x: 101, y: 101 }) } }, - position: { x: 0, y: 0 }, + viewer.raiseEvent('canvas-click', { + eventSource: { viewport: viewer.viewport }, + position: new OpenSeadragon.Point(101, 101), }); expect(selectAnnotation).toHaveBeenCalledWith('base', 'http://example.org/identifier/annotation/anno-line'); @@ -291,47 +276,44 @@ describe('AnnotationsOverlay', () => { describe('onCanvasMouseMove', () => { it('triggers the hover event for every annotation at that point', () => { + jest.useFakeTimers(); const hoverAnnotation = jest.fn(); - const forceRedraw = jest.fn(); - viewer.forceRedraw = forceRedraw; - viewer.viewport = { pointFromPixel: point => ({ x: 101, y: 101 }) }; - - wrapper.setProps( - { - annotations: [ - new AnnotationList( - { + const { viewer } = createWrapper({ + annotations: [ + new AnnotationList( + { + '@id': 'foo', + resources: [{ '@id': 'foo', - resources: [{ - '@id': 'foo', - '@type': 'oa:Annotation', - motivation: 'sc:painting', - on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=100,100,250,20', - }, { - '@id': 'bar', - '@type': 'oa:Annotation', - motivation: 'sc:painting', - on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=0,0,250,250', - }, { - '@id': 'irrelevant-box', - '@type': 'oa:Annotation', - motivation: 'sc:painting', - on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=0,0,50,50', - }], - }, - ), - ], - hoverAnnotation, - }, - ); + '@type': 'oa:Annotation', + motivation: 'sc:painting', + on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=100,100,250,20', + }, { + '@id': 'bar', + '@type': 'oa:Annotation', + motivation: 'sc:painting', + on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=0,0,250,250', + }, { + '@id': 'irrelevant-box', + '@type': 'oa:Annotation', + motivation: 'sc:painting', + on: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json#xywh=0,0,50,50', + }], + }, + ), + ], + hoverAnnotation, + }); - wrapper.instance().onCanvasMouseMove({ - position: { x: 0, y: 0 }, + viewer.raiseEvent('mouse-move', { + position: new OpenSeadragon.Point(101, 101), }); - wrapper.instance().onCanvasMouseMove.flush(); + jest.advanceTimersByTime(20); expect(hoverAnnotation).toHaveBeenCalledWith('base', ['foo', 'bar']); + + jest.useRealTimers(); }); }); }); diff --git a/__tests__/src/components/App.test.js b/__tests__/src/components/App.test.js index 2d758ec5725fd1ee666a121c865450cc407ab19a..868c73ad9c59d9a71a54fdfaee10aad7a43fe2be 100644 --- a/__tests__/src/components/App.test.js +++ b/__tests__/src/components/App.test.js @@ -1,11 +1,10 @@ -import { shallow } from 'enzyme'; -import PluginProvider from '../../../src/extend/PluginProvider'; -import AppProviders from '../../../src/containers/AppProviders'; +import { render, screen } from 'test-utils'; + import { App } from '../../../src/components/App'; /** */ function createWrapper(props) { - return shallow( + return render( <App {...props} />, @@ -13,10 +12,12 @@ function createWrapper(props) { } describe('App', () => { - it('should render all needed elements ', () => { - const wrapper = createWrapper(); - expect(wrapper.find(PluginProvider).length).toBe(1); - expect(wrapper.find(AppProviders).length).toBe(1); - expect(wrapper.find('Suspense').length).toBe(1); + it('should asynchronously render all needed elements', async () => { + createWrapper(); + + expect(screen.queryByRole('main')).not.toBeInTheDocument(); + await screen.findByText('welcome'); + + expect(screen.getByRole('main')).toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/AppProviders.test.js b/__tests__/src/components/AppProviders.test.js index 1ce84d264b218d771b70abef4960c03c547296e8..ae97556638812588c2481172ad0e7bc3943d9a23 100644 --- a/__tests__/src/components/AppProviders.test.js +++ b/__tests__/src/components/AppProviders.test.js @@ -1,67 +1,86 @@ -import { shallow } from 'enzyme'; -import { ThemeProvider, StylesProvider } from '@material-ui/core/styles'; -import { DndContext, DndProvider } from 'react-dnd'; +import Button from '@mui/material/Button'; +import { render, screen } from 'test-utils'; +import { useTranslation } from 'react-i18next'; +import { useDrop } from 'react-dnd'; import { AppProviders } from '../../../src/components/AppProviders'; import settings from '../../../src/config/settings'; jest.unmock('react-i18next'); /** */ -function createWrapper(props) { - return shallow( +function MockTranslationComponent() { + const { t } = useTranslation(); + return <div data-testid="test-translation">{t('aboutMirador')}</div>; +} + +/** */ +function MockDnDComponent() { + try { + const drop = useDrop(() => ({ + accept: 'box', + drop: () => ({ name: 'Mirador Test' }), + }))[1]; // the drop ref is the 2nd arg of returned array + return ( + <div ref={drop} data-testid="test-dnd"> + Test DnD + </div> + ); + } catch (e) { + // do nothing + } + // We have to return something to render; can't throw an error in catch + return <div data-testid="failed-dnd" />; +} + +/** */ +function createWrapper(props = {}) { + return render( <AppProviders - language="en" isFullscreenEnabled={false} theme={settings.theme} - translations={{}} - t={k => k} + translations={{ + de: { aboutMirador: 'Über Mirador' }, + en: { aboutMirador: 'About Project Mirador' }, + }} {...props} - />, + > + <Button color="primary" data-testid="test-button">Test</Button> + <MockTranslationComponent /> + <MockDnDComponent /> + </AppProviders>, + { + preloadedState: { + config: { + availableLanguages: { de: 'Deutsch', en: 'English' }, + themes: { a: {} }, + }, + }, + }, ); } describe('AppProviders', () => { - it('should render all needed elements ', () => { - const wrapper = createWrapper(); - expect(wrapper.find(ThemeProvider).length).toBe(1); - expect(wrapper.find(StylesProvider).length).toBe(1); - }); - - it('sets up a theme based on the config passed in merged w/ MaterialUI', () => { - const wrapper = createWrapper(); - const { theme } = wrapper.find(ThemeProvider).props(); - expect(theme.palette.type).toEqual('light'); - expect(theme.typography.useNextVariants).toBe(true); - expect(Object.keys(theme).length).toBeGreaterThan(10); + it('sets up the configured theme', () => { + createWrapper(); + // #1967d2 is the set as the primary text color in our settings.theme + expect(screen.getByTestId('test-button')).toHaveStyle('color: #1967d2'); }); - - it('sets up translations based on the config passed in', () => { - const wrapper = createWrapper({ translations: { en: { off: 'on' } } }); - expect(wrapper.instance().i18n.t('off')).toEqual('on'); + it('provides a drag and drop context', async () => { + createWrapper(); + // AppProvider provides a default dndManager if it is undefined in props + // This component will not render without a drag drop context + expect(screen.getByTestId('test-dnd')).toBeInTheDocument(); }); - - describe('componentDidUpdate()', () => { - it('changes the i18n language if the language prop has been updated', () => { - const wrapper = createWrapper(); - - expect(wrapper.instance().i18n.language).toEqual('en'); - wrapper.setProps({ language: 'de' }); - expect(wrapper.instance().i18n.language).toEqual('de'); - }); - }); - - it('provides a drag and drop context', () => { - const wrapper = createWrapper(); - expect(wrapper.find('MaybeDndProvider').dive().find(DndProvider).length).toBe(1); - }); - it('allows apps to opt-out of the drag and drop provider', () => { - const wrapper = createWrapper({ dndManager: false }); - expect(wrapper.find('MaybeDndProvider').dive().find(DndProvider).length).toBe(0); + createWrapper({ dndManager: false }); + expect(screen.getByTestId('failed-dnd')).toBeInTheDocument(); }); - - it('allows apps to provide an existing drag and drop context', () => { - const wrapper = createWrapper({ dndManager: 'whatever' }); - expect(wrapper.find('MaybeDndProvider').dive().find(DndContext.Provider).prop('value')).toBe('whatever'); + it('displays the default language if none set', () => { + createWrapper(); + expect(screen.getByTestId('test-translation')).toHaveTextContent('About Project Mirador'); + }); + it('displays the specified language translations', async () => { + createWrapper({ language: 'de' }); + expect(await screen.findByTestId('test-translation')).toHaveTextContent('Über Mirador'); }); }); diff --git a/__tests__/src/components/AttributionPanel.test.js b/__tests__/src/components/AttributionPanel.test.js index af822aa54f3968fa37a27aabd8be1d7efc92d1e0..f67927d82333c0328b11c6bf8370b38aabb1e74c 100644 --- a/__tests__/src/components/AttributionPanel.test.js +++ b/__tests__/src/components/AttributionPanel.test.js @@ -1,76 +1,55 @@ -import { shallow } from 'enzyme'; -import Typography from '@material-ui/core/Typography'; -import Link from '@material-ui/core/Link'; -import { Img } from 'react-image'; +/** + * @jest-environment-options { "resources": "usable" } + */ +import { render, screen, waitFor } from 'test-utils'; + import { AttributionPanel } from '../../../src/components/AttributionPanel'; -import { LabelValueMetadata } from '../../../src/components/LabelValueMetadata'; /** * Helper function to create a shallow wrapper around AttributionPanel */ function createWrapper(props) { - return shallow( + return render( <AttributionPanel id="xyz" t={str => str} windowId="window" {...props} />, + { preloadedState: { companionWindows: { xyz: { content: 'attribution' } } } }, ); } describe('AttributionPanel', () => { it('renders the required statement', () => { const requiredStatement = [ - { label: 'x', value: 'y' }, + { label: 'required statement', values: ['must be shown'] }, ]; - const wrapper = createWrapper({ requiredStatement }); - expect(wrapper.find(LabelValueMetadata).length).toBe(1); - }); + createWrapper({ requiredStatement }); - it('renders the rights statement', () => { - const wrapper = createWrapper({ rights: ['http://example.com', 'http://stanford.edu'] }); - expect( - wrapper.find(Typography).at(0).matchesElement( - <Typography>rights</Typography>, - ), - ).toBe(true); - expect( - wrapper.find(Typography).at(1).matchesElement( - <Typography> - <Link href="http://example.com">http://example.com</Link> - </Typography>, - ), - ).toBe(true); - expect( - wrapper.find(Typography).at(2).matchesElement( - <Typography> - <Link href="http://stanford.edu">http://stanford.edu</Link> - </Typography>, - ), - ).toBe(true); + expect(screen.getByText('required statement')).toBeInTheDocument(); + expect(screen.getByText('must be shown')).toBeInTheDocument(); }); it('renders the rights statement', () => { - const wrapper = createWrapper({ rights: [] }); - expect( - wrapper.find(Typography).length, - ).toBe(0); + createWrapper({ rights: ['http://example.com', 'http://stanford.edu'] }); + + expect(screen.getByText('rights')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'http://example.com' })).toHaveAttribute('href', 'http://example.com'); + expect(screen.getByRole('link', { name: 'http://stanford.edu' })).toHaveAttribute('href', 'http://stanford.edu'); }); - it('renders the manifest logo', () => { - const manifestLogo = 'http://example.com'; - const wrapper = createWrapper({ manifestLogo }); - expect(wrapper.find(Img).length).toBe(1); - expect(wrapper.find(Img).props().src).toEqual([manifestLogo]); + it('does not render the rights statement if it is empty', () => { + createWrapper({ rights: [] }); + expect(screen.queryByText('rights')).not.toBeInTheDocument(); }); - describe('when metadata is not present', () => { - it('does not render empty elements elements', () => { - const wrapper = createWrapper({}); - expect(wrapper.find(LabelValueMetadata).length).toBe(0); - expect(wrapper.find(Typography).length).toBe(0); - expect(wrapper.find(Img).length).toBe(0); - }); + it('renders the manifest logo', async () => { + const manifestLogo = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mMMDQmtBwADgwF/Op8FmAAAAABJRU5ErkJggg=='; + + const { container } = createWrapper({ manifestLogo }); + await waitFor(() => { expect(container.querySelector('img')).toBeInTheDocument(); }); // eslint-disable-line testing-library/no-container, testing-library/no-node-access + + expect(container.querySelector('img')).toHaveAttribute('src', manifestLogo); // eslint-disable-line testing-library/no-container, testing-library/no-node-access }); }); diff --git a/__tests__/src/components/AudioViewer.test.js b/__tests__/src/components/AudioViewer.test.js index a74b8bfc3ef8a7c0a21dea7c3268a6157f214685..9db307ec8d139f10e81c7e20d7eb466abf47862a 100644 --- a/__tests__/src/components/AudioViewer.test.js +++ b/__tests__/src/components/AudioViewer.test.js @@ -1,40 +1,43 @@ -import { shallow } from 'enzyme'; +import { render, screen } from 'test-utils'; import { AudioViewer } from '../../../src/components/AudioViewer'; /** create wrapper */ function createWrapper(props, suspenseFallback) { - return shallow( + return render( <AudioViewer classes={{}} - audioOptions={{ crossOrigin: 'anonymous' }} + audioOptions={{ crossOrigin: 'anonymous', 'data-testid': 'audio' }} {...props} />, ); } +/* eslint-disable testing-library/no-node-access */ describe('AudioViewer', () => { - let wrapper; describe('render', () => { it('audioResources', () => { - wrapper = createWrapper({ + createWrapper({ audioResources: [ { getFormat: () => 'video/mp4', id: 1 }, { getFormat: () => 'video/mp4', id: 2 }, ], }, true); - expect(wrapper.contains(<source src={1} type="video/mp4" />)).toBe(true); - expect(wrapper.contains(<source src={2} type="video/mp4" />)).toBe(true); + const audio = screen.getByTestId('audio'); + + expect(audio.querySelector('source:nth-of-type(1)')).toHaveAttribute('src', '1'); + expect(audio.querySelector('source:nth-of-type(2)')).toHaveAttribute('src', '2'); }); it('passes through configurable options', () => { - wrapper = createWrapper({ + createWrapper({ audioResources: [ { getFormat: () => 'audio/mp3', id: 1 }, ], }, true); - expect(wrapper.exists('audio[crossOrigin="anonymous"]')).toBe(true); // eslint-disable-line jsx-a11y/media-has-caption + + expect(screen.getByTestId('audio')).toHaveAttribute('crossOrigin', 'anonymous'); }); it('captions', () => { - wrapper = createWrapper({ + createWrapper({ audioResources: [ { getFormat: () => 'video/mp4', id: 1 }, ], @@ -43,8 +46,12 @@ describe('AudioViewer', () => { { getDefaultLabel: () => 'French', getProperty: () => 'fr', id: 2 }, ], }, true); - expect(wrapper.contains(<track src={1} label="English" srcLang="en" />)).toBe(true); - expect(wrapper.contains(<track src={2} label="French" srcLang="fr" />)).toBe(true); + const audio = screen.getByTestId('audio'); + + expect(audio.querySelector('track:nth-of-type(1)')).toHaveAttribute('srcLang', 'en'); + expect(audio.querySelector('track:nth-of-type(1)')).toHaveAttribute('label', 'English'); + expect(audio.querySelector('track:nth-of-type(2)')).toHaveAttribute('srcLang', 'fr'); + expect(audio.querySelector('track:nth-of-type(2)')).toHaveAttribute('label', 'French'); }); }); }); diff --git a/__tests__/src/components/BackgroundPluginArea.test.js b/__tests__/src/components/BackgroundPluginArea.test.js index bbe31fd5d336a01cbb5bc6295ce8385485d5da9a..c7844c5341bea2d0f6b9e1a8493a261a6d3ae9a8 100644 --- a/__tests__/src/components/BackgroundPluginArea.test.js +++ b/__tests__/src/components/BackgroundPluginArea.test.js @@ -1,9 +1,14 @@ -import { shallow } from 'enzyme'; +import { render, screen } from 'test-utils'; import { BackgroundPluginArea } from '../../../src/components/BackgroundPluginArea'; -import { PluginHook } from '../../../src/components/PluginHook'; -it('renders the component', () => { - const wrapper = shallow(<BackgroundPluginArea />); - expect(wrapper.find('.mirador-background-plugin-area').length).toBe(1); - expect(wrapper.find(PluginHook).length).toBe(1); +/** */ +const mockComponent = () => ( + <div data-testid="test" /> +); + +describe('BackgroundPluginArea', () => { + it('renders the component', () => { + render(<BackgroundPluginArea PluginComponents={[mockComponent]} />); + expect(screen.getByTestId('test')).toBeInTheDocument(); + }); }); diff --git a/__tests__/src/components/Branding.test.js b/__tests__/src/components/Branding.test.js index fbbfc6c7d881e6d11a2ed3b0a270f2439e854a7f..33a6f8ae9ee5e17b8ed887ab7733f7f3a27716f1 100644 --- a/__tests__/src/components/Branding.test.js +++ b/__tests__/src/components/Branding.test.js @@ -1,44 +1,18 @@ -import { shallow } from 'enzyme'; -import Typography from '@material-ui/core/Typography'; -import IconButton from '@material-ui/core/IconButton'; -import MiradorIcon from '../../../src/components/icons/MiradorIcon'; +import { render, screen, within } from 'test-utils'; import { Branding } from '../../../src/components/Branding'; describe('Branding', () => { - let wrapper; - it('renders', () => { - wrapper = shallow(<Branding />); + render(<Branding />); - expect( - wrapper.matchesElement( - <div> - <Typography> - <IconButton> - <MiradorIcon /> - </IconButton> - </Typography> - </div>, - ), - ).toBe(true); + expect(screen.getByRole('link')).toHaveAttribute('href', 'https://projectmirador.org'); + expect(within(screen.getByRole('link')).getByRole('img')).toBeInTheDocument(); }); it('renders additional items for the wide variant', () => { - wrapper = shallow(<Branding variant="wide" />); + render(<Branding variant="wide" />); - expect( - wrapper.matchesElement( - <div> - <div> - <Typography>mirador</Typography> - </div> - <Typography> - <IconButton> - <MiradorIcon /> - </IconButton> - </Typography> - </div>, - ), - ).toBe(true); + expect(screen.getByText('mirador')).toBeInTheDocument(); + expect(screen.getByRole('link')).toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/CanvasAnnotations.test.js b/__tests__/src/components/CanvasAnnotations.test.js index a51a744c98f675b78f083f9d0346f7aafb32df64..eef2ad78f7f43a28c9d4de1bffbd991c7d0d8632 100644 --- a/__tests__/src/components/CanvasAnnotations.test.js +++ b/__tests__/src/components/CanvasAnnotations.test.js @@ -1,14 +1,13 @@ -import { shallow } from 'enzyme'; -import Typography from '@material-ui/core/Typography'; -import Chip from '@material-ui/core/Chip'; -import MenuList from '@material-ui/core/MenuList'; -import MenuItem from '@material-ui/core/MenuItem'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; +import i18next from 'i18next'; + import { CanvasAnnotations } from '../../../src/components/CanvasAnnotations'; import { ScrollTo } from '../../../src/components/ScrollTo'; /** Utility function to wrap CanvasAnnotations */ function createWrapper(props) { - return shallow( + return render( <CanvasAnnotations classes={{}} deselectAnnotation={() => {}} @@ -16,7 +15,7 @@ function createWrapper(props) { index={0} label="A Canvas Label" selectAnnotation={() => {}} - t={(key, args) => ({ args, key })} + t={i18next.t} totalSize={1} windowId="abc" {...props} @@ -41,28 +40,30 @@ describe('CanvasAnnotations', () => { }, ]; - it('renders a heading with the appropriate context based on index and totalSize', () => { - wrapper = createWrapper({ annotations }); + it('renders a heading for a single item', () => { + createWrapper({ annotations }); - expect(wrapper.find(Typography).length).toBe(1); - let heading = wrapper.find(Typography).props().children; - expect(heading.key).toEqual('annotationCanvasLabel'); - expect(heading.args.label).toEqual('A Canvas Label'); - expect(heading.args.context).toEqual('1/1'); + expect(screen.getByText('Item: [A Canvas Label]')).toBeInTheDocument(); + }); + it('renders a heading with the appropriate context based on index and totalSize', () => { wrapper = createWrapper({ annotations, index: 1, totalSize: 2 }); - heading = wrapper.find(Typography).props().children; - expect(heading.args.context).toEqual('2/2'); + expect(screen.getByText('Right: [A Canvas Label]')).toBeInTheDocument(); + + wrapper.unmount(); + + createWrapper({ annotations, index: 0, totalSize: 2 }); + expect(screen.getByText('Left: [A Canvas Label]')).toBeInTheDocument(); }); it('renders a List w/ a ListItem for every annotation', () => { - wrapper = createWrapper({ annotations }); + createWrapper({ annotations }); - expect(wrapper.find(MenuList).length).toEqual(1); - expect(wrapper.find(MenuItem).length).toEqual(2); + expect(screen.getByRole('menu')).toBeInTheDocument(); + expect(screen.getAllByRole('menuitem').length).toEqual(2); }); - it('scrolls to the selected annotation', () => { + xit('scrolls to the selected annotation', () => { wrapper = createWrapper({ annotations, selectedAnnotationId: 'abc123' }); expect(wrapper.find(ScrollTo).length).toEqual(2); @@ -71,31 +72,35 @@ describe('CanvasAnnotations', () => { }); it('renders a Chip for every tag', () => { - wrapper = createWrapper({ annotations }); + createWrapper({ annotations }); - expect(wrapper.find(Chip).length).toEqual(2); + expect(screen.getByText('abc123', { container: 'span.MuiChip-label' })).toBeInTheDocument(); + expect(screen.getByText('def456', { container: 'span.MuiChip-label' })).toBeInTheDocument(); }); it('renders nothing when there are no annotations', () => { - wrapper = createWrapper(); - expect(wrapper.find(Typography).length).toBe(0); + createWrapper(); + expect(screen.queryByRole('menuitem')).not.toBeInTheDocument(); }); describe('interacting with annotations', () => { - it('triggers the selectAnnotation prop with the correct arguments when clicking an unselected annotation', () => { + it('triggers the selectAnnotation prop with the correct arguments when clicking an unselected annotation', async () => { const selectAnnotation = jest.fn(); + const user = userEvent.setup(); wrapper = createWrapper({ annotations, selectAnnotation, }); - wrapper.find(MenuItem).first().simulate('click'); + await user.click(screen.getByRole('menuitem', { name: /First Annotation/ })); + expect(selectAnnotation).toHaveBeenCalledWith('abc', 'abc123'); }); - it('triggers the deselectAnnotation prop with the correct arguments when clicking a selected annotation', () => { + it('triggers the deselectAnnotation prop with the correct arguments when clicking a selected annotation', async () => { const deselectAnnotation = jest.fn(); + const user = userEvent.setup(); wrapper = createWrapper({ annotations, @@ -103,12 +108,14 @@ describe('CanvasAnnotations', () => { selectedAnnotationId: 'abc123', }); - wrapper.find(MenuItem).first().simulate('click'); + await user.click(screen.getByRole('menuitem', { name: /First Annotation/ })); + expect(deselectAnnotation).toHaveBeenCalledWith('abc', 'abc123'); }); - it('highlights annotations on mouse enter', () => { + it('highlights annotations on mouse enter', async () => { const hoverAnnotation = jest.fn(); + const user = userEvent.setup(); wrapper = createWrapper({ annotations: [ @@ -122,12 +129,14 @@ describe('CanvasAnnotations', () => { hoverAnnotation, }); - wrapper.find(MenuItem).first().simulate('mouseEnter'); + await user.hover(screen.getByRole('menuitem', { name: /Annotation/ })); + expect(hoverAnnotation).toHaveBeenCalledWith('abc', ['annoId']); }); - it('highlights annotations on focus', () => { + it('highlights annotations on focus', async () => { const hoverAnnotation = jest.fn(); + const user = userEvent.setup(); wrapper = createWrapper({ annotations: [ @@ -137,16 +146,24 @@ describe('CanvasAnnotations', () => { tags: [], targetId: 'example.com/iiif/12345', }, + { + content: 'Annotation2', + id: 'annoId2', + tags: [], + targetId: 'example.com/iiif/12345', + }, ], hoverAnnotation, }); - wrapper.find(MenuItem).first().simulate('focus'); - expect(hoverAnnotation).toHaveBeenCalledWith('abc', ['annoId']); + await user.keyboard('{ArrowDown}'); + + expect(hoverAnnotation).toHaveBeenCalledWith('abc', ['annoId2']); }); - it('sets the highlighted annotation to null on mouse leave', () => { + it('sets the highlighted annotation to null on mouse leave', async () => { const hoverAnnotation = jest.fn(); + const user = userEvent.setup(); wrapper = createWrapper({ annotations: [ @@ -159,8 +176,11 @@ describe('CanvasAnnotations', () => { ], hoverAnnotation, }); + await user.hover(screen.getByRole('menuitem', { name: /Annotation/ })); + + expect(hoverAnnotation).toHaveBeenCalledWith('abc', ['annoId']); - wrapper.find(MenuItem).first().simulate('mouseLeave'); + await user.hover(screen.getByRole('menu')); expect(hoverAnnotation).toHaveBeenCalledWith('abc', []); }); }); diff --git a/__tests__/src/components/CanvasInfo.test.js b/__tests__/src/components/CanvasInfo.test.js index ba83097303ecc4f7259916f3e0b55487cd9bafb1..a8af19bacd15ef3577afc198c66e2591c8b20962 100644 --- a/__tests__/src/components/CanvasInfo.test.js +++ b/__tests__/src/components/CanvasInfo.test.js @@ -1,17 +1,15 @@ -import { shallow } from 'enzyme'; -import Typography from '@material-ui/core/Typography'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { CanvasInfo } from '../../../src/components/CanvasInfo'; -import { LabelValueMetadata } from '../../../src/components/LabelValueMetadata'; -import CollapsibleSection from '../../../src/containers/CollapsibleSection'; -import SanitizedHtml from '../../../src/containers/SanitizedHtml'; describe('CanvasInfo', () => { - const metadata = [{ label: {}, value: {} }]; - let wrapper; + const metadata = [{ label: 'some label', values: ['some value'] }]; + let user; describe('when metadata is present', () => { beforeEach(() => { - wrapper = shallow( + user = userEvent.setup(); + render( <CanvasInfo canvasLabel="The Canvas Label" canvasDescription="The Canvas Description" @@ -22,46 +20,38 @@ describe('CanvasInfo', () => { ); }); - it('renders the content in a CollapsibleSection', () => { - expect(wrapper.find(CollapsibleSection).length).toBe(1); + it('renders the content in a CollapsibleSection', async () => { + expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('currentItem'); + expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent(/The Canvas Label/); + + await user.click(screen.getByRole('button')); + + expect(screen.queryByRole('heading', { level: 5 })).not.toBeInTheDocument(); }); it('renders canvas label', () => { - expect( - wrapper.find(Typography).at(0).matchesElement( - <Typography>The Canvas Label</Typography>, - ), - ).toBe(true); + expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent(/The Canvas Label/); }); - it('renders canvas description in SanitizedHtml component', () => { - expect( - wrapper.find(Typography).at(1).matchesElement( - <Typography> - <SanitizedHtml htmlString="The Canvas Description" ruleSet="iiif" /> - </Typography>, - ), - ).toBe(true); + it('renders canvas description', () => { + expect(screen.getByText('The Canvas Description')).toBeInTheDocument(); }); it('renders canvas metadata in LabelValueMetadata component', () => { - expect( - wrapper.find(LabelValueMetadata).at(0).matchesElement( - <LabelValueMetadata labelValuePairs={metadata} />, - ), - ).toBe(true); + expect(screen.getByText('some label')).toBeInTheDocument(); + expect(screen.getByText('some value')).toBeInTheDocument(); }); }); describe('when metadata is not present', () => { beforeEach(() => { - wrapper = shallow( + render( <CanvasInfo id="xyz" />, ); }); it('does not render empty elements elements', () => { - expect(wrapper.find(LabelValueMetadata).length).toBe(0); + expect(screen.queryByRole('heading', { level: 5 })).not.toBeInTheDocument(); }); }); }); diff --git a/__tests__/src/components/CanvasLayers.test.js b/__tests__/src/components/CanvasLayers.test.js index fd5f337b7bb315320cc067272e94cdd22790f722..1d5133fafcb97543f70dcc629934131e53508343 100644 --- a/__tests__/src/components/CanvasLayers.test.js +++ b/__tests__/src/components/CanvasLayers.test.js @@ -1,14 +1,12 @@ -import { shallow } from 'enzyme'; -import Input from '@material-ui/core/Input'; -import Slider from '@material-ui/core/Slider'; -import Typography from '@material-ui/core/Typography'; -import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; +import { screen, fireEvent, render } from 'test-utils'; +import userEvent from '@testing-library/user-event'; +import { Resource } from 'manifesto.js'; + import { CanvasLayers } from '../../../src/components/CanvasLayers'; -import IIIFThumbnail from '../../../src/containers/IIIFThumbnail'; /** Utility function to wrap CanvasAnnotations */ function createWrapper(props) { - return shallow( + return render( <CanvasLayers canvasId="foo" classes={{}} @@ -28,57 +26,54 @@ function createWrapper(props) { describe('CanvasLayers', () => { describe('with multiple canvases', () => { it('displays the canvas label', () => { - const wrapper = createWrapper({ totalSize: 2 }); + createWrapper({ totalSize: 2 }); - expect(wrapper.find(Typography).text()).toEqual('annotationCanvasLabel'); + expect(screen.getByText('annotationCanvasLabel', { container: '.MuiTypography-overline' })).toBeInTheDocument(); }); }); it('renders canvas layers in a list', () => { - const wrapper = createWrapper({ + createWrapper({ canvasId: 'https://prtd.app/hamilton/canvas/p1.json', layers: [ - { id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg' }, - { id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png' }, + new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg' }, {}), + new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png' }, {}), ], }); - const inner = shallow( - wrapper.find(Droppable).prop('children')({}, {}), - ); - - expect(inner.find(Draggable).length).toEqual(2); - expect(inner.find(Draggable).at(0).prop('draggableId')).toEqual('https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg'); - expect(inner.find(Draggable).at(1).prop('draggableId')).toEqual('https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png'); + expect(screen.getAllByRole('listitem')[0]).toHaveTextContent('1'); + expect(screen.getAllByRole('listitem')[1]).toHaveTextContent('2'); - const layer = shallow( - inner.find(Draggable).at(0).prop('children')({}, {}), - ); - - expect(layer.find(IIIFThumbnail).prop('resource').id).toEqual('https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg'); - expect(layer.find(Typography).text()).toEqual('1'); - expect(layer.find('[aria-label="layer_hide"]').length).toEqual(1); - expect(layer.find('[aria-label="layer_moveToTop"]').length).toEqual(1); - expect(layer.find('[title="layer_opacity"]').length).toEqual(1); + expect(screen.getAllByRole('button', { name: 'layer_hide' }).length).toEqual(2); + expect(screen.getAllByRole('button', { name: 'layer_moveToTop' }).length).toEqual(2); + expect(screen.getAllByRole('spinbutton', { name: 'layer_opacity' }).length).toEqual(2); }); - it('handles drag + drop of layers', () => { + it('handles drag + drop of layers', async () => { const updateLayers = jest.fn(); - - const wrapper = createWrapper({ + createWrapper({ + canvasId: 'foo', layers: [ - { id: 'a' }, - { id: 'b' }, + new Resource({ id: 'a' }, {}), + new Resource({ id: 'b' }, {}), ], updateLayers, }); - const { droppableId } = wrapper.instance(); + const buttons = screen.getAllByRole('button'); + const layer = buttons.find(b => b.getAttribute('data-rfd-drag-handle-draggable-id') === 'b'); - wrapper.find(DragDropContext).simulate('dragEnd', { - destination: { droppableId, index: 0 }, - source: { droppableId, index: 1 }, - }); + layer.focus(); + + // TODO: user-event doesn't believe in sending keycode values + // (https://github.com/testing-library/user-event/issues/842) + // but beautiful-dnd requires them. So this doesn't work: + // await user.keyboard('{Space}'); + // await user.keyboard('{ArrowUp}'); + + fireEvent.keyDown(layer, { code: 'Space', keyCode: 32 }); + fireEvent.keyDown(layer, { code: 'ArrowUp', keyCode: 38 }); + fireEvent.keyDown(layer, { code: 'Space', keyCode: 32 }); expect(updateLayers).toHaveBeenCalledWith('abc', 'foo', { a: { index: 1 }, @@ -87,24 +82,24 @@ describe('CanvasLayers', () => { }); describe('actions', () => { - let layer; let updateLayers; + let user; beforeEach(() => { updateLayers = jest.fn(); - const wrapper = createWrapper({ + user = userEvent.setup(); + createWrapper({ canvasId: 'https://prtd.app/hamilton/canvas/p1.json', layers: [ - { id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg' }, - { id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png' }, + new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg' }, {}), + new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png' }, {}), ], updateLayers, }); - layer = shallow(wrapper.instance().renderLayer({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png' }, 1)); }); - it('has a button for moving a layer to the top', () => { - layer.find('[aria-label="layer_moveToTop"]').simulate('click'); + it('has a button for moving a layer to the top', async () => { + await user.click(screen.getAllByLabelText('layer_moveToTop')[1]); expect(updateLayers).toHaveBeenCalledWith('abc', 'https://prtd.app/hamilton/canvas/p1.json', { 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg': { @@ -116,8 +111,8 @@ describe('CanvasLayers', () => { }); }); - it('has a button for toggling visibility', () => { - layer.find('[aria-label="layer_hide"]').simulate('click'); + it('has a button for toggling visibility', async () => { + await user.click(screen.getAllByLabelText('layer_hide')[1]); expect(updateLayers).toHaveBeenCalledWith('abc', 'https://prtd.app/hamilton/canvas/p1.json', { 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png': { @@ -126,8 +121,11 @@ describe('CanvasLayers', () => { }); }); - it('has a slider to changing layer opacity', () => { - layer.find(Slider).simulate('change', {}, 50); + xit('has a slider to changing layer opacity', async () => { + const target = screen.getAllByRole('slider')[1]; + await user.click(target); + await user.type(target, '{Space}'); + await user.type(target, '{ArrowLeft}'); expect(updateLayers).toHaveBeenCalledWith('abc', 'https://prtd.app/hamilton/canvas/p1.json', { 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png': { @@ -137,11 +135,11 @@ describe('CanvasLayers', () => { }); it('has a text input to changing layer opacity', () => { - layer.find(Input).simulate('change', { target: { value: 70 } }); + fireEvent.change(screen.getAllByRole('spinbutton')[1], { target: { value: '90' } }); expect(updateLayers).toHaveBeenCalledWith('abc', 'https://prtd.app/hamilton/canvas/p1.json', { 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png': { - opacity: 0.7, + opacity: 0.9, }, }); }); diff --git a/__tests__/src/components/ChangeThemeDialog.test.js b/__tests__/src/components/ChangeThemeDialog.test.js index f2bf0d2688c04888ddb070db08f6374bd0b247fd..906a52fb29bf981181c4122c26570ece98c34e30 100644 --- a/__tests__/src/components/ChangeThemeDialog.test.js +++ b/__tests__/src/components/ChangeThemeDialog.test.js @@ -1,17 +1,16 @@ -import { shallow } from 'enzyme'; -import Dialog from '@material-ui/core/Dialog'; -import ListItemText from '@material-ui/core/ListItemText'; -import MenuItem from '@material-ui/core/MenuItem'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { ChangeThemeDialog } from '../../../src/components/ChangeThemeDialog'; /** * Helper function to create a shallow wrapper around ErrorDialog */ function createWrapper(props) { - return shallow( + render( <ChangeThemeDialog classes={{ darkColor: '#000000', lightColor: '#ffffff' }} handleClose={() => {}} + open setSelectedTheme={() => {}} t={t => (t)} selectedTheme="light" @@ -22,45 +21,45 @@ function createWrapper(props) { } describe('ChangeThemeDialog', () => { - let wrapper; + it('renders nothing when the dialog is not open', () => { + createWrapper({ open: false }); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); it('renders propertly', () => { - wrapper = createWrapper(); - - expect(wrapper.find(Dialog).length).toBe(1); + createWrapper(); + expect(screen.getByRole('dialog')).toBeInTheDocument(); }); it('shows up theme selection properly', () => { - wrapper = createWrapper(); + createWrapper(); + + const menuItems = screen.queryAllByRole('menuitem'); - expect(wrapper.find(ListItemText).length).toBe(2); - expect(wrapper.find(ListItemText).first().render().text()).toBe('light'); - expect(wrapper.find(ListItemText).last().render().text()).toBe('dark'); + expect(menuItems.length).toBe(2); + expect(menuItems[0]).toHaveTextContent('light'); + expect(menuItems[1]).toHaveTextContent('dark'); }); - it('shows up theme selection properly', () => { + it('shows up theme selection properly', async () => { + const user = userEvent.setup(); const setSelectedTheme = jest.fn(); - wrapper = createWrapper({ setSelectedTheme }); - wrapper.find(MenuItem).first().simulate('click'); + createWrapper({ setSelectedTheme }); + const menuItem = screen.getByRole('menuitem', { name: 'light' }); + expect(menuItem).toBeInTheDocument(); + + await user.click(menuItem); expect(setSelectedTheme).toHaveBeenCalledWith('light'); }); describe('inital focus', () => { - const mockMenuItemFocus = jest.fn(); - const mockMenu = { - querySelectorAll: (selector) => { - expect(selector).toEqual('li[value="light"]'); - return [{ focus: mockMenuItemFocus }]; - }, - }; - - it('sets an onEntered prop on the Dialog that focuses the selected item', () => { - wrapper = createWrapper(); + it('focuses the selected item', () => { + createWrapper({ selectedTheme: 'light' }); - wrapper.find(Dialog).props().onEntered(mockMenu); - expect(mockMenuItemFocus).toHaveBeenCalled(); + const menuItem = screen.getByRole('menuitem', { name: 'light' }); + expect(menuItem).toHaveFocus(); }); }); }); diff --git a/__tests__/src/components/CollapsibleSection.test.js b/__tests__/src/components/CollapsibleSection.test.js index 1db02cd182e54efd0effe42f456d5bd754279180..aad2dd0f4c16bbce9bfa1a7e826d4cf525baae70 100644 --- a/__tests__/src/components/CollapsibleSection.test.js +++ b/__tests__/src/components/CollapsibleSection.test.js @@ -1,13 +1,12 @@ -import { shallow } from 'enzyme'; -import Typography from '@material-ui/core/Typography'; -import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { CollapsibleSection } from '../../../src/components/CollapsibleSection'; /** * Helper function to create a shallow wrapper around CollapsibleSection */ function createWrapper(props) { - return shallow( + return render( <CollapsibleSection classes={{}} id="abc123" @@ -15,49 +14,29 @@ function createWrapper(props) { t={k => k} {...props} > - <span>Child content</span> + <span data-testid="child">Child content</span> </CollapsibleSection>, ); } describe('CollapsibleSection', () => { - let wrapper; beforeEach(() => { - wrapper = createWrapper(); + createWrapper(); }); it('renders the passed in label is a Typography', () => { - expect(wrapper.find(Typography).props().children).toEqual('The Section Label'); + expect(screen.getByRole('heading')).toHaveTextContent('The Section Label'); }); - it('renders a mirador button with an icon dependent on the open state', () => { - expect(wrapper.state().open).toBe(true); - expect(wrapper.find('pure(KeyboardArrowUpSharpIcon)')); - wrapper.setState({ open: false }); - expect(wrapper.find('pure(KeyboardArrowDownSharpIcon)')); + it('renders the appropriate i18n label based on open state', async () => { + expect(screen.getByRole('button')).toHaveAttribute('aria-label', 'collapseSection'); + await userEvent.click(screen.getByRole('button')); + expect(screen.getByRole('button')).toHaveAttribute('aria-label', 'expandSection'); }); - it('renders the appropriate i18n label based on open state', () => { - expect(wrapper.state().open).toBe(true); - expect(wrapper.find(MiradorMenuButton).props()['aria-label']).toEqual('collapseSection'); - expect(wrapper.find(MiradorMenuButton).prop('aria-expanded')).toEqual(true); - wrapper.setState({ open: false }); - expect(wrapper.find(MiradorMenuButton).props()['aria-label']).toEqual('expandSection'); - expect(wrapper.find(MiradorMenuButton).prop('aria-expanded')).toEqual(false); - }); - - it('renders children based on the open state', () => { - expect(wrapper.state().open).toBe(true); - expect(wrapper.find('Fragment').props().children[1]).toEqual(<span>Child content</span>); - wrapper.setState({ open: false }); - expect(wrapper.find('Fragment').props().children[1]).toBe(false); - }); - - it('toggles the children based on MiradorMenuButton/Typography click', () => { - expect(wrapper.find('Fragment').props().children[1]).toEqual(<span>Child content</span>); - wrapper.find(Typography).simulate('click'); - expect(wrapper.find('Fragment').props().children[1]).toBe(false); - wrapper.find(MiradorMenuButton).simulate('click'); - expect(wrapper.find('Fragment').props().children[1]).toEqual(<span>Child content</span>); + it('displays children based on the open state', async () => { + expect(screen.getByTestId('child')).toBeVisible(); + await userEvent.click(screen.getByRole('button')); + expect(screen.queryByTestId('child')).not.toBeVisible(); }); }); diff --git a/__tests__/src/components/CollectionDialog.test.js b/__tests__/src/components/CollectionDialog.test.js index 693282f658780d163856560686390c7c58866994..f8e7afa90ed45044550880db09946ab91ce5eaae 100644 --- a/__tests__/src/components/CollectionDialog.test.js +++ b/__tests__/src/components/CollectionDialog.test.js @@ -1,44 +1,51 @@ -import { shallow } from 'enzyme'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import Button from '@material-ui/core/Button'; -import MenuItem from '@material-ui/core/MenuItem'; -import Skeleton from '@material-ui/lab/Skeleton'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { Utils } from 'manifesto.js'; + import { CollectionDialog } from '../../../src/components/CollectionDialog'; import collection from '../../fixtures/version-2/collection.json'; /** */ function createWrapper(props) { const manifest = Utils.parseManifest(props.manifest ? props.manifest : collection); - CollectionDialog.prototype.dialogContainer = () => true; - return shallow( + + render(<div id="window" />); + + return render( <CollectionDialog addWindow={() => {}} classes={{}} ready manifest={manifest} t={(key) => key} + windowId="window" {...props} />, + { preloadedState: { windows: { window: { id: 'window' } } } }, ); } describe('CollectionDialog', () => { it('renders a dialog with collection menu items', () => { - const wrapper = createWrapper({}); - expect(wrapper.find(Dialog).length).toEqual(1); - expect(wrapper.find(MenuItem).length).toEqual(55); - expect(wrapper.find(MenuItem).first().text()).toEqual('Test 1 Manifest: Minimum Required Fields'); + createWrapper({}); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getAllByRole('menuitem')).toHaveLength(55); + expect(screen.getByRole('menuitem', { name: 'Test 1 Manifest: Minimum Required Fields' })).toBeInTheDocument(); }); it('when not ready returns placeholder skeleton', () => { - const wrapper = createWrapper({ ready: false }); - expect(wrapper.find(Skeleton).length).toEqual(3); + createWrapper({ ready: false }); + + expect(screen.queryByRole('menuitem')).not.toBeInTheDocument(); + + expect(screen.getByRole('dialog').querySelectorAll('.MuiSkeleton-root')).toHaveLength(3); // eslint-disable-line testing-library/no-node-access }); - it('clicking the hide button fires hideCollectionDialog', () => { + it('clicking the hide button fires hideCollectionDialog', async () => { + const user = userEvent.setup(); const hideCollectionDialog = jest.fn(); - const wrapper = createWrapper({ hideCollectionDialog }); - expect(wrapper.find(DialogActions).find(Button).first().simulate('click')); + createWrapper({ hideCollectionDialog }); + + await user.click(screen.getByRole('button', { name: 'close' })); expect(hideCollectionDialog).toHaveBeenCalled(); }); }); diff --git a/__tests__/src/components/CollectionInfo.test.js b/__tests__/src/components/CollectionInfo.test.js index d768432032e005026f4d203236e2b577fa4e486b..bbeb3d6b85f2d7fe12160f7f10f7f0d80f8e45a0 100644 --- a/__tests__/src/components/CollectionInfo.test.js +++ b/__tests__/src/components/CollectionInfo.test.js @@ -1,11 +1,10 @@ -import { shallow } from 'enzyme'; -import Button from '@material-ui/core/Button'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { CollectionInfo } from '../../../src/components/CollectionInfo'; -import CollapsibleSection from '../../../src/containers/CollapsibleSection'; /** */ function createWrapper(props) { - return shallow( + return render( <CollectionInfo id="test" collectionPath={[1, 2]} @@ -16,18 +15,28 @@ function createWrapper(props) { } describe('CollectionInfo', () => { - it('renders a collapsible section', () => { - const wrapper = createWrapper(); - expect(wrapper.find(CollapsibleSection).length).toEqual(1); + it('renders a collapsible section', async () => { + const user = userEvent.setup(); + createWrapper(); + + expect(screen.getByRole('heading', { name: 'collection' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'showCollection' })).toBeVisible(); + + await user.click(screen.getByRole('button', { name: 'collapseSection' })); + + expect(screen.queryByRole('button', { name: 'showCollection' })).not.toBeInTheDocument(); }); it('without a collectionPath, renders nothing', () => { const wrapper = createWrapper({ collectionPath: [] }); - expect(wrapper.find(CollapsibleSection).length).toEqual(0); + expect(wrapper.container).toBeEmptyDOMElement(); }); - it('clicking the button fires showCollectionDialog', () => { + it('clicking the button fires showCollectionDialog', async () => { + const user = userEvent.setup(); const showCollectionDialog = jest.fn(); - const wrapper = createWrapper({ showCollectionDialog }); - expect(wrapper.find(Button).first().simulate('click')); + + createWrapper({ showCollectionDialog }); + + await user.click(screen.getByRole('button', { name: 'showCollection' })); expect(showCollectionDialog).toHaveBeenCalled(); }); }); diff --git a/__tests__/src/components/CompanionArea.test.js b/__tests__/src/components/CompanionArea.test.js index 9e7981b0bc0bd0857c0dd81eb6d008ee81a0ac4f..53e7bcdb0a5d1e347e664b42f4fe945c78371c2d 100644 --- a/__tests__/src/components/CompanionArea.test.js +++ b/__tests__/src/components/CompanionArea.test.js @@ -1,14 +1,11 @@ -import { shallow } from 'enzyme'; -import Slide from '@material-ui/core/Slide'; -import ArrowLeftIcon from '@material-ui/icons/ArrowLeftSharp'; -import ArrowRightIcon from '@material-ui/icons/ArrowRightSharp'; -import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; + import { CompanionArea } from '../../../src/components/CompanionArea'; -import CompanionWindowFactory from '../../../src/containers/CompanionWindowFactory'; /** */ function createWrapper(props) { - return shallow( + return render( <CompanionArea classes={{ horizontal: 'horizontal' }} direction="ltr" @@ -19,109 +16,85 @@ function createWrapper(props) { t={key => key} {...props} />, + { preloadedState: { companionWindows: { baz: { content: 'attribution' }, foo: { content: 'info' } } } }, ); } describe('CompanionArea', () => { it('should render all <CompanionWindow>', () => { - const wrapper = createWrapper(); - expect(wrapper.find(CompanionWindowFactory).length).toBe(2); - expect(wrapper.find(Slide).prop('direction')).toBe('left'); - }); + createWrapper(); - it('when rtl, the left slide should be from the right', () => { - const wrapper = createWrapper({ - direction: 'rtl', - }); - expect(wrapper.find(CompanionWindowFactory).length).toBe(2); - expect(wrapper.find(Slide).prop('direction')).toBe('right'); - }); - - it('when rtl, the right slide should be from the left', () => { - const wrapper = createWrapper({ - direction: 'rtl', - position: 'left', - }); - expect(wrapper.find(CompanionWindowFactory).length).toBe(2); - expect(wrapper.find(Slide).prop('direction')).toBe('left'); + expect(screen.getAllByRole('complementary')).toHaveLength(2); }); it('should add the appropriate classes when the companion area fills the full width', () => { - const wrapper = createWrapper({ position: 'bottom' }); - expect(wrapper.find('div.horizontal').length).toBe(2); - expect(wrapper.find(Slide).prop('direction')).toBe('up'); + const { container } = createWrapper({ position: 'bottom' }); + + expect(container.querySelector('.mirador-companion-area-bottom')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access, testing-library/no-container }); - it('should pass correct props to the <CompanionWindow> components', () => { - const wrapper = createWrapper(); - const props = wrapper.find(CompanionWindowFactory).at(0).props(); - expect(props.id).toBe('foo'); - expect(props.windowId).toBe('abc123'); + it('renders the appropriate <CompanionWindow> components', () => { + createWrapper(); + + expect(screen.getByRole('heading', { name: 'aboutThisItem' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'attributionTitle' })).toBeInTheDocument(); }); - it('has a toggle to show the companion area window in the left position', () => { + it('has a toggle to show the companion area window in the left position', async () => { const setCompanionAreaOpen = jest.fn(); + const user = userEvent.setup(); - const wrapper = createWrapper({ + createWrapper({ companionAreaOpen: false, position: 'left', setCompanionAreaOpen, sideBarOpen: true, }); - expect(wrapper.find(MiradorMenuButton).length).toBe(1); - expect(wrapper.find(MiradorMenuButton).first().children(ArrowRightIcon).length).toBe(1); - expect(wrapper.find(Slide).prop('direction')).toBe('right'); - expect(wrapper.find(MiradorMenuButton).prop('aria-expanded')).toBe(false); - expect(wrapper.find('div.mirador-companion-windows').length).toBe(1); - expect(wrapper.find('div.mirador-companion-windows').props().style.display).toBe('none'); + expect(screen.getByRole('button', { name: 'expandSidePanel' })).toHaveAttribute('aria-expanded', 'false'); + expect(screen.queryByRole('complementary')).not.toBeInTheDocument(); - wrapper.find(MiradorMenuButton).first().props().onClick(); // Trigger the onClick prop + await user.click(screen.getByRole('button', { name: 'expandSidePanel' })); expect(setCompanionAreaOpen).toHaveBeenCalledWith('abc123', true); }); - it('has a toggle to hide the companion area window in the left position', () => { + it('has a toggle to hide the companion area window in the left position', async () => { const setCompanionAreaOpen = jest.fn(); + const user = userEvent.setup(); - const wrapper = createWrapper({ + createWrapper({ companionAreaOpen: true, position: 'left', setCompanionAreaOpen, sideBarOpen: true, }); - expect(wrapper.find(MiradorMenuButton).length).toBe(1); - expect(wrapper.find(MiradorMenuButton).first().children(ArrowLeftIcon).length).toBe(1); - expect(wrapper.find(MiradorMenuButton).prop('aria-expanded')).toBe(true); - - expect(wrapper.find('div.mirador-companion-windows').length).toBe(1); - expect(wrapper.find('div.mirador-companion-windows').props().style.display).toBe('flex'); - - wrapper.find(MiradorMenuButton).first().props().onClick(); // Trigger the onClick prop + expect(screen.getByRole('button', { name: 'collapseSidePanel' })).toHaveAttribute('aria-expanded', 'true'); + await user.click(screen.getByRole('button', { name: 'collapseSidePanel' })); expect(setCompanionAreaOpen).toHaveBeenCalledWith('abc123', false); }); it('does not show a toggle if the sidebar is collapsed', () => { - const wrapper = createWrapper({ + createWrapper({ companionAreaOpen: true, position: 'left', setCompanionAreaOpen: () => {}, sideBarOpen: false, }); - expect(wrapper.find(MiradorMenuButton).length).toBe(0); + expect(screen.queryByRole('button', { name: 'collapseSidePanel' })).not.toBeInTheDocument(); }); it('does not show a toggle in other positions', () => { - const wrapper = createWrapper({ + createWrapper({ companionAreaOpen: true, position: 'whatever', setCompanionAreaOpen: () => {}, sideBarOpen: true, }); - expect(wrapper.find(MiradorMenuButton).length).toBe(0); + expect(screen.queryByRole('button', { name: 'collapseSidePanel' })).not.toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/CompanionWindow.test.js b/__tests__/src/components/CompanionWindow.test.js index 99c5b8583a1e142c51f2333e0f698a41cf1d4d8c..a7fe7b71ef7f4c22b3a3838545c029845a11e84d 100644 --- a/__tests__/src/components/CompanionWindow.test.js +++ b/__tests__/src/components/CompanionWindow.test.js @@ -1,16 +1,16 @@ -import { shallow } from 'enzyme'; -import { Rnd } from 'react-rnd'; -import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; +import { render, screen } from 'test-utils'; +import PropTypes from 'prop-types'; +import userEvent from '@testing-library/user-event'; import { CompanionWindow } from '../../../src/components/CompanionWindow'; /** create wrapper */ function createWrapper(props) { - return shallow( + return render( <CompanionWindow id="abc123" + isDisplayed direction="ltr" windowId="x" - classes={{ horizontal: 'horizontal', small: 'small', vertical: 'vertical' }} companionWindow={{}} position="right" {...props} @@ -19,119 +19,125 @@ function createWrapper(props) { } describe('CompanionWindow', () => { - let companionWindow; describe('aria-label', () => { it('has an aria-label for the landmark derived from the title', () => { - companionWindow = createWrapper({ title: 'some title' }); - expect(companionWindow.prop('aria-label')).toEqual('some title'); + createWrapper({ title: 'some title' }); + + expect(screen.getByRole('complementary')).toHaveAccessibleName('some title'); }); it('can be overridden with an explicit ariaLabel prop', () => { - companionWindow = createWrapper({ ariaLabel: 'some label', title: 'some title' }); - expect(companionWindow.prop('aria-label')).toEqual('some label'); + createWrapper({ ariaLabel: 'some label', title: 'some title' }); + + expect(screen.getByRole('complementary')).toHaveAccessibleName('some label'); }); }); describe('when the openInCompanionWindow button is clicked', () => { - it('passes the the updateCompanionWindow prop to MiradorMenuButton with the appropriate args', () => { + it('passes the the updateCompanionWindow prop to MiradorMenuButton with the appropriate args', async () => { const updateCompanionWindow = jest.fn(); - companionWindow = createWrapper({ + const user = userEvent.setup(); + + createWrapper({ position: 'left', updateCompanionWindow, }); - const button = companionWindow.find(MiradorMenuButton); - button.props().onClick(); // Trigger the onClick prop - expect(updateCompanionWindow).toHaveBeenCalledTimes(1); + await user.click(screen.getByRole('button', { name: 'openInCompanionWindow' })); + expect(updateCompanionWindow).toHaveBeenCalledWith({ position: 'right' }); }); }); describe('when the close companion window button is clicked', () => { - it('triggers the onCloseClick prop with the appropriate args', () => { + it('triggers the onCloseClick prop with the appropriate args', async () => { const removeCompanionWindowEvent = jest.fn(); - companionWindow = createWrapper({ + const user = userEvent.setup(); + + createWrapper({ onCloseClick: removeCompanionWindowEvent, }); - const button = companionWindow.find(MiradorMenuButton); - button.props().onClick(); // Trigger the onClick prop - expect(removeCompanionWindowEvent).toHaveBeenCalledTimes(1); - }); + await user.click(screen.getByRole('button', { name: 'closeCompanionWindow' })); - it('allows the children to know about onCloseClick', () => { - const removeCompanionWindowEvent = jest.fn(); - companionWindow = createWrapper({ - children: <div>HelloWorld</div>, - onCloseClick: removeCompanionWindowEvent, - }); - const { parentactions } = companionWindow.children().find('div').props(); - parentactions.closeCompanionWindow(); expect(removeCompanionWindowEvent).toHaveBeenCalledTimes(1); }); - it('checks that a child is valid before enhancing', () => { + it('allows the children to know about onCloseClick', async () => { const removeCompanionWindowEvent = jest.fn(); - companionWindow = createWrapper({ - children: [null, <div>HelloWorld</div>], + const user = userEvent.setup(); + + /** Some child component */ + const Button = ({ parentactions, ...props }) => ( + <button type="button" onClick={parentactions.closeCompanionWindow} {...props}>Close</button> + ); + + Button.propTypes = { + parentactions: PropTypes.shape({ closeCompanionWindow: PropTypes.func.isRequired }).isRequired, + }; + + createWrapper({ + children: <Button data-testid="button" />, onCloseClick: removeCompanionWindowEvent, }); - const { parentactions } = companionWindow.children().find('div').props(); - parentactions.closeCompanionWindow(); + + await user.click(screen.getByTestId('button')); expect(removeCompanionWindowEvent).toHaveBeenCalledTimes(1); }); }); describe('when the companion window is on the right', () => { - const updateCompanionWindow = jest.fn(); - companionWindow = createWrapper({ - position: 'right', - updateCompanionWindow, - }); + it('can be moved to the bottom', async () => { + const updateCompanionWindow = jest.fn(); + const user = userEvent.setup(); - expect(companionWindow.find('.vertical').length).toBe(1); + createWrapper({ + position: 'right', + updateCompanionWindow, + }); + + expect(screen.getByRole('complementary')).toHaveClass('mirador-companion-window-right'); + + await user.click(screen.getByRole('button', { name: 'moveCompanionWindowToBottom' })); - const button = companionWindow.find(MiradorMenuButton).first(); - button.props().onClick(); // Trigger the onClick prop - expect(updateCompanionWindow).toHaveBeenCalledTimes(1); - expect(updateCompanionWindow).toHaveBeenCalledWith({ position: 'bottom' }); + expect(updateCompanionWindow).toHaveBeenCalledWith({ position: 'bottom' }); + }); }); describe('when the companion window is on the bottom', () => { - const updateCompanionWindow = jest.fn(); - companionWindow = createWrapper({ - position: 'bottom', - updateCompanionWindow, - }); + it('can be moved to the right', async () => { + const updateCompanionWindow = jest.fn(); + const user = userEvent.setup(); + + createWrapper({ + position: 'bottom', + updateCompanionWindow, + }); - expect(companionWindow.find('.horizontal').length).toBe(1); + expect(screen.getByRole('complementary')).toHaveClass('mirador-companion-window-bottom '); - const button = companionWindow.find(MiradorMenuButton).first(); - button.props().onClick(); // Trigger the onClick prop - expect(updateCompanionWindow).toHaveBeenCalledTimes(1); - expect(updateCompanionWindow).toHaveBeenCalledWith({ position: 'right' }); + await user.click(screen.getByRole('button', { name: 'moveCompanionWindowToRight' })); + + expect(updateCompanionWindow).toHaveBeenCalledWith({ position: 'right' }); + }); }); it('renders title controls when available', () => { - companionWindow = createWrapper({ position: 'bottom', titleControls: <div className="xyz" /> }); - expect(companionWindow.find('.mirador-companion-window-title-controls div.xyz').length).toBe(1); + createWrapper({ position: 'bottom', titleControls: <div data-testid="xyz" /> }); - companionWindow = createWrapper({ position: 'bottom' }); - expect(companionWindow.find('.mirador-companion-window-title-controls').length).toBe(0); + expect(screen.getByTestId('xyz')).toBeInTheDocument(); }); - it('adds a small class when the component width is small', () => { - companionWindow = createWrapper({ size: { width: 369 } }); - expect(companionWindow.find('.small').length).toBe(1); + it('has a resize handler', () => { + const { container } = createWrapper(); + + expect(container.querySelector('.react-draggable')).toHaveStyle({ height: '100%', width: '235px' }); // eslint-disable-line testing-library/no-node-access, testing-library/no-container + expect(container.querySelector('[style*="cursor: col-resize;"]')).toHaveStyle({ left: '-5px' }); // eslint-disable-line testing-library/no-node-access, testing-library/no-container }); - it('has a resize handler ', () => { - companionWindow = createWrapper(); - expect(companionWindow.find(Rnd).length).toBe(1); - expect(companionWindow.find(Rnd).prop('enableResizing').left).toBe(true); - expect(companionWindow.find(Rnd).prop('default')).toEqual({ height: '100%', width: 235 }); - - companionWindow = createWrapper({ position: 'bottom' }); - expect(companionWindow.find(Rnd).length).toBe(1); - expect(companionWindow.find(Rnd).prop('enableResizing').top).toBe(true); - expect(companionWindow.find(Rnd).prop('default')).toEqual({ height: 201, width: 'auto' }); + + it('has a vertical resize handle when position is bottom', () => { + const { container } = createWrapper({ position: 'bottom' }); + + expect(container.querySelector('.react-draggable')).toHaveStyle({ height: '201px', width: 'auto' }); // eslint-disable-line testing-library/no-node-access, testing-library/no-container + expect(container.querySelector('[style*="cursor: row-resize;"]')).toHaveStyle({ top: '-5px' }); // eslint-disable-line testing-library/no-node-access, testing-library/no-container }); }); diff --git a/__tests__/src/components/CompanionWindowFactory.test.js b/__tests__/src/components/CompanionWindowFactory.test.js index acf0d39718561f11a001777108587f9647c5366e..5019630aa50cb36492697ac96dc26c9b1f4f5028 100644 --- a/__tests__/src/components/CompanionWindowFactory.test.js +++ b/__tests__/src/components/CompanionWindowFactory.test.js @@ -1,106 +1,88 @@ -import { shallow } from 'enzyme'; -import WindowSideBarInfoPanel from '../../../src/containers/WindowSideBarInfoPanel'; -import WindowSideBarCanvasPanel from '../../../src/containers/WindowSideBarCanvasPanel'; -import WindowSideBarAnnotationsPanel from '../../../src/containers/WindowSideBarAnnotationsPanel'; -import ThumbnailNavigation from '../../../src/containers/ThumbnailNavigation'; -import AttributionPanel from '../../../src/containers/AttributionPanel'; -import SearchPanel from '../../../src/containers/SearchPanel'; -import LayersPanel from '../../../src/containers/LayersPanel'; -import CustomPanel from '../../../src/containers/CustomPanel'; +import { render, screen } from 'test-utils'; + import { CompanionWindowFactory } from '../../../src/components/CompanionWindowFactory'; /** create wrapper */ -function createWrapper(props) { - return shallow( +function createWrapper({ content = 'closed', ...props }) { + return render( <CompanionWindowFactory windowId="x" id="123" - content="closed" + content={content} {...props} />, + { preloadedState: { companionWindows: { 123: { content }, thumb: {} }, windows: { x: { thumbnailNavigationId: 'thumb' } } } }, ); } describe('CompanionWindowFactory', () => { - let wrapper; - describe('for an info window', () => { it('renders the appropriate arg component', () => { - wrapper = createWrapper({ + createWrapper({ content: 'info', }); - expect(wrapper.find(WindowSideBarInfoPanel).length).toBe(1); + expect(screen.getByRole('heading', { level: 3 })).toHaveAccessibleName('aboutThisItem'); }); }); describe('for a canvas navigation window', () => { it('renders the appropriate arg component', () => { - wrapper = createWrapper({ + createWrapper({ content: 'canvas', }); - expect(wrapper.find(WindowSideBarCanvasPanel).length).toBe(1); + expect(screen.getByRole('heading', { level: 3 })).toHaveAccessibleName('canvasIndex'); }); }); describe('for an annotation window', () => { it('renders the appropriate arg component', () => { - wrapper = createWrapper({ + createWrapper({ content: 'annotations', }); - expect(wrapper.find(WindowSideBarAnnotationsPanel).length).toBe(1); + expect(screen.getByRole('heading', { level: 3 })).toHaveAccessibleName('annotations'); }); }); describe('for an attribution window', () => { it('renders the appropriate arg component', () => { - wrapper = createWrapper({ + createWrapper({ content: 'attribution', }); - expect(wrapper.find(AttributionPanel).length).toBe(1); + expect(screen.getByRole('heading', { level: 3 })).toHaveAccessibleName('attributionTitle'); }); }); describe('for the thumbnail nav window', () => { it('renders the appropriate arg component', () => { - wrapper = createWrapper({ + createWrapper({ content: 'thumbnailNavigation', }); - expect(wrapper.find(ThumbnailNavigation).length).toBe(1); + expect(screen.getByRole('grid')).toHaveAccessibleName('thumbnailNavigation'); }); }); describe('for the search window', () => { it('renders the appropriate arg component', () => { - wrapper = createWrapper({ + createWrapper({ content: 'search', }); - expect(wrapper.find(SearchPanel).length).toBe(1); + expect(screen.getByRole('heading', { level: 3 })).toHaveAccessibleName('searchTitle'); }); }); describe('for the layers window', () => { it('renders the appropriate arg component', () => { - wrapper = createWrapper({ + createWrapper({ content: 'layers', }); - expect(wrapper.find(LayersPanel).length).toBe(1); - }); - }); - - describe('for a custom panel', () => { - it('renders the appropriate arg component', () => { - wrapper = createWrapper({ - content: 'custom', - }); - - expect(wrapper.find(CustomPanel).length).toBe(1); + expect(screen.getByRole('heading', { level: 3 })).toHaveAccessibleName('layers'); }); }); }); diff --git a/__tests__/src/components/ErrorContent.test.js b/__tests__/src/components/ErrorContent.test.js new file mode 100644 index 0000000000000000000000000000000000000000..8662d26cb75dbecad4bfebbec2166242bb1d262c --- /dev/null +++ b/__tests__/src/components/ErrorContent.test.js @@ -0,0 +1,65 @@ +import i18next from 'i18next'; +import { render, screen } from 'test-utils'; + +import { ErrorContent } from '../../../src/components/ErrorContent'; + +describe('ErrorContent', () => { + it('should render everything when showJsError is true', async () => { + render( + <ErrorContent + error={new Error('Invalid JSON')} + windowId="xyz" + manifestId="foo" + classes={{}} + t={i18next.t} + />, + { + preloadedState: { + config: { + window: { + showJsError: true, + }, + }, + windows: { + xyz: { + collectionDialogOn: false, + companionWindowIds: [], + }, + }, + }, + }, + ); + expect(screen.getByRole('alert')).toBeInTheDocument(); + expect(screen.getByText('An error occurred')).toBeInTheDocument(); + expect(screen.getByText('Technical details')).toBeInTheDocument(); + expect(document.querySelector('pre')).toHaveTextContent('Invalid JSON'); // eslint-disable-line testing-library/no-node-access + }); + it('does not render the alert when showJsError is false ', async () => { + render( + <ErrorContent + error={new Error('Invalid JSON')} + windowId="xyz" + manifestId="foo" + showJsError={false} + classes={{}} + t={i18next.t} + />, + { + preloadedState: { + config: { + window: { + showJsError: false, + }, + }, + windows: { + xyz: { + collectionDialogOn: false, + companionWindowIds: [], + }, + }, + }, + }, + ); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + }); +}); diff --git a/__tests__/src/components/ErrorDialog.test.js b/__tests__/src/components/ErrorDialog.test.js index 64a32a1e5740c6118ea6c14e7687519f7baa887a..650f4df5e2cd376d937cf17aa4e3099bc918e0d3 100644 --- a/__tests__/src/components/ErrorDialog.test.js +++ b/__tests__/src/components/ErrorDialog.test.js @@ -1,14 +1,12 @@ -import { shallow } from 'enzyme'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import DialogContentText from '@material-ui/core/DialogContentText'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { ErrorDialog } from '../../../src/components/ErrorDialog'; /** * Helper function to create a shallow wrapper around ErrorDialog */ function createWrapper(props) { - return shallow( + return render( <ErrorDialog t={key => key} {...props} @@ -17,29 +15,31 @@ function createWrapper(props) { } describe('ErrorDialog', () => { - let wrapper; - it('renders properly', () => { const error = { id: 'testid123', message: '' }; - wrapper = createWrapper({ error }); - expect(wrapper.find(Dialog).length).toBe(1); + createWrapper({ error }); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByRole('heading')).toHaveTextContent('errorDialogTitle'); }); it('shows up error message correctly', () => { const errorMessage = 'error testMessage 123'; const error = { id: 'testid123', message: errorMessage }; - wrapper = createWrapper({ error }); - expect(wrapper.find(DialogContentText).find('[variant="body2"]').render().text()).toBe(errorMessage); + createWrapper({ error }); + expect(screen.getByRole('dialog')).toHaveTextContent(errorMessage); }); - it('triggers the handleClick prop when clicking the ok button', () => { + it('triggers the handleClick prop when clicking the ok button', async () => { const error = { id: 'testid123', message: '' }; const mockHandleClick = jest.fn(); + const user = userEvent.setup(); + + createWrapper({ error, removeError: mockHandleClick }); - wrapper = createWrapper({ error, removeError: mockHandleClick }); - wrapper.find(Button).simulate('click'); + await user.click(screen.getByRole('button', { name: 'errorDialogConfirm' })); expect(mockHandleClick).toHaveBeenCalledTimes(1); }); }); diff --git a/__tests__/src/components/FullScreenButton.test.js b/__tests__/src/components/FullScreenButton.test.js index b15f28aa3d018cdb9b76c65d95846dc6830dbff0..6b28ddaf52f934d77a8422ceffe597c22d5bebc7 100644 --- a/__tests__/src/components/FullScreenButton.test.js +++ b/__tests__/src/components/FullScreenButton.test.js @@ -1,76 +1,64 @@ -import { shallow } from 'enzyme'; -import FullscreenIcon from '@material-ui/icons/FullscreenSharp'; -import FullscreenExitIcon from '@material-ui/icons/FullscreenExitSharp'; -import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; -import { FullScreenButton } from '../../../src/components/FullScreenButton'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import FullScreenContext from '../../../src/contexts/FullScreenContext'; +import { FullScreenButton } from '../../../src/components/FullScreenButton'; /** */ function createWrapper(props, contextProps = { active: false }) { - return shallow( - <FullScreenButton - classes={{}} - className="xyz" - {...props} - />, - { - wrappingComponent: FullScreenContext.Provider, - wrappingComponentProps: { value: { enter: () => { }, exit: () => { }, ...contextProps } }, - }, - ).dive(); + return render( + <FullScreenContext.Provider value={{ enter: () => { }, exit: () => { }, ...contextProps }}> + <FullScreenButton + classes={{}} + className="xyz" + {...props} + /> + </FullScreenContext.Provider>, + ); } describe('FullScreenButton', () => { - let wrapper; - let menuButton; - it('renders without an error', () => { - wrapper = createWrapper(); + createWrapper(); - expect(wrapper.find(MiradorMenuButton).length).toBe(1); - expect(wrapper.find(MiradorMenuButton).prop('className')).toBe('xyz'); + expect(screen.getByRole('button')).toHaveClass('xyz'); }); describe('when not in fullscreen', () => { let enter; - beforeAll(() => { + let user; + beforeEach(() => { enter = jest.fn(); - wrapper = createWrapper({}, { enter }); - menuButton = wrapper.find(MiradorMenuButton); - }); - - it('has the FullscreenIcon', () => { - expect(menuButton.children(FullscreenIcon).length).toBe(1); + user = userEvent.setup(); + createWrapper({}, { enter }); }); it('has the proper aria-label i18n key', () => { - expect(menuButton.props()['aria-label']).toEqual('workspaceFullScreen'); + expect(screen.getByRole('button')).toHaveAccessibleName('workspaceFullScreen'); }); - it('triggers the handle enter with the appropriate boolean', () => { - menuButton.props().onClick(); // Trigger the onClick prop + it('triggers the handle enter with the appropriate boolean', async () => { + await user.click(screen.getByRole('button')); + expect(enter).toHaveBeenCalled(); }); }); describe('when in fullscreen', () => { let exit; - beforeAll(() => { + let user; + beforeEach(() => { exit = jest.fn(); - wrapper = createWrapper({}, { active: true, exit }); - menuButton = wrapper.find(MiradorMenuButton); - }); - - it('has the FullscreenExitIcon', () => { - expect(menuButton.children(FullscreenExitIcon).length).toBe(1); + user = userEvent.setup(); + createWrapper({}, { active: true, exit }); }); it('has the proper aria-label', () => { - expect(menuButton.props()['aria-label']).toEqual('exitFullScreen'); + expect(screen.getByRole('button')).toHaveAccessibleName('exitFullScreen'); }); - it('triggers the handle exit with the appropriate boolean', () => { - menuButton.props().onClick(); // Trigger the onClick prop + it('triggers the handle exit with the appropriate boolean', async () => { + await user.click(screen.getByRole('button')); + expect(exit).toHaveBeenCalled(); }); }); diff --git a/__tests__/src/components/GalleryView.test.js b/__tests__/src/components/GalleryView.test.js index 3cb9b74342c65c15f793eb3b4721ce927e5e4929..48eef4d4e87262b3ec6af4c82215ecdf6d008caa 100644 --- a/__tests__/src/components/GalleryView.test.js +++ b/__tests__/src/components/GalleryView.test.js @@ -1,13 +1,12 @@ -import { shallow } from 'enzyme'; +import { render, screen } from 'test-utils'; import { Utils } from 'manifesto.js'; -import Paper from '@material-ui/core/Paper'; + import manifestJson from '../../fixtures/version-2/019.json'; import { GalleryView } from '../../../src/components/GalleryView'; -import GalleryViewThumbnail from '../../../src/containers/GalleryViewThumbnail'; /** create wrapper */ function createWrapper(props) { - return shallow( + return render( <GalleryView canvases={Utils.parseManifest(manifestJson).getSequences()[0].getCanvases()} windowId="1234" @@ -19,28 +18,26 @@ function createWrapper(props) { describe('GalleryView', () => { let setCanvas; - let wrapper; beforeEach(() => { setCanvas = jest.fn(); - wrapper = createWrapper({ setCanvas }); }); it('renders the component', () => { - expect(wrapper.find(Paper).length).toBe(1); - expect(wrapper.find(Paper).prop('component')).toEqual('section'); + const { container } = createWrapper({ setCanvas }); + expect(container.querySelector('section')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access, testing-library/no-container }); it('renders gallery items for all canvases', () => { - expect(wrapper.find(GalleryViewThumbnail).length).toBe(3); + createWrapper({ setCanvas }); + const buttons = screen.queryAllByRole('button'); + expect(buttons.length).toBe(3); }); describe('when viewingDirection="right-to-left"', () => { - beforeEach(() => { - wrapper = createWrapper({ + it('sets up Paper to be rtl', () => { + createWrapper({ viewingDirection: 'right-to-left', }); - }); - - it('sets up Paper to be rtl', () => { - expect(wrapper.find('WithStyles(ForwardRef(Paper))').props().dir).toEqual('rtl'); + const buttons = screen.queryAllByRole('button'); + expect(buttons[0].closest('section')).toHaveAttribute('dir', 'rtl'); // eslint-disable-line testing-library/no-node-access }); }); }); diff --git a/__tests__/src/components/GalleryViewThumbnail.test.js b/__tests__/src/components/GalleryViewThumbnail.test.js index c15632938b55c1c3ea71828ec4a1068ad300a190..bad9e3098edd648ca859b814766c6ddfff075a09 100644 --- a/__tests__/src/components/GalleryViewThumbnail.test.js +++ b/__tests__/src/components/GalleryViewThumbnail.test.js @@ -1,17 +1,16 @@ -import { shallow } from 'enzyme'; +import { render, fireEvent, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { Utils } from 'manifesto.js'; -import Chip from '@material-ui/core/Chip'; -import { InView } from 'react-intersection-observer'; +import { mockAllIsIntersecting } from 'react-intersection-observer/test-utils'; + import manifestJson from '../../fixtures/version-2/019.json'; import { GalleryViewThumbnail } from '../../../src/components/GalleryViewThumbnail'; -import IIIFThumbnail from '../../../src/containers/IIIFThumbnail'; /** create wrapper */ function createWrapper(props) { - return shallow( + return render( <GalleryViewThumbnail canvas={Utils.parseManifest(manifestJson).getSequences()[0].getCanvases()[0]} - classes={{ selected: 'selected' }} focusOnCanvas={() => {}} setCanvas={() => {}} {...props} @@ -20,113 +19,110 @@ function createWrapper(props) { } describe('GalleryView', () => { - let wrapper; - it('sets a mirador-current-canvas-grouping class if the canvas is selected', () => { - wrapper = createWrapper({ selected: true }); - expect(wrapper.find('div[role="button"]').at(0).prop('className')).toEqual('selected'); - - wrapper = createWrapper({ selected: false }); - expect(wrapper.find('div[role="button"]').at(0).prop('className')).not.toEqual('selected'); + beforeEach(() => { + window.HTMLElement.prototype.scrollIntoView = jest.fn(); }); it('renders the thumbnail', () => { - wrapper = createWrapper({ config: { height: 55 } }); - expect(wrapper.find(IIIFThumbnail).length).toBe(1); - expect(wrapper.find(IIIFThumbnail).prop('maxHeight')).toBe(55); + createWrapper({ config: { height: 55 } }); + expect(screen.getByRole('presentation')).toBeInTheDocument(); + expect(screen.getByRole('presentation')).toHaveStyle('height: 55px'); }); - it('sets the selected canvas on click', () => { + it('sets the selected canvas on click', async () => { const setCanvas = jest.fn(); - wrapper = createWrapper({ setCanvas }); - wrapper.find('div[role="button"]').first().simulate('click'); + createWrapper({ setCanvas }); + const user = userEvent.setup(); + await user.click(screen.getByRole('button')); expect(setCanvas).toHaveBeenCalledWith('http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json'); }); - it('sets the window mode if the selected canvas is clicked', () => { + it('sets the window mode if the selected canvas is clicked', async () => { const focusOnCanvas = jest.fn(); - wrapper = createWrapper({ focusOnCanvas, selected: true }); - wrapper.find('div[role="button"]').first().simulate('click'); + createWrapper({ focusOnCanvas, selected: true }); + const user = userEvent.setup(); + await user.click(screen.getByRole('button')); expect(focusOnCanvas).toHaveBeenCalled(); }); it('sets the window mode if the user hits enter while on a canvas', () => { const focusOnCanvas = jest.fn(); - wrapper = createWrapper({ focusOnCanvas, selected: true }); - wrapper.find('div[role="button"]').first().simulate('keyUp', { key: 'Enter' }); + createWrapper({ focusOnCanvas, selected: true }); + const button = screen.getByRole('button'); + button.focus(); + fireEvent.keyUp(button, { code: 'Enter', key: 'Enter' }); expect(focusOnCanvas).toHaveBeenCalled(); }); it('sets the window mode if the user hits space while on a canvas', () => { const focusOnCanvas = jest.fn(); - wrapper = createWrapper({ focusOnCanvas, selected: true }); - wrapper.find('div[role="button"]').first().simulate('keyUp', { key: ' ' }); + createWrapper({ focusOnCanvas, selected: true }); + const button = screen.getByRole('button'); + button.focus(); + fireEvent.keyUp(button, { code: ' ', key: ' ' }); expect(focusOnCanvas).toHaveBeenCalled(); }); it('sets the canvas if the user hits a key (non-space or non-enter) while on a canvas', () => { const setCanvas = jest.fn(); - wrapper = createWrapper({ selected: true, setCanvas }); - wrapper.find('div[role="button"]').first().simulate('keyUp', { key: 'd' }); + createWrapper({ selected: true, setCanvas }); + const button = screen.getByRole('button'); + button.focus(); + fireEvent.keyUp(button, { code: 'd', key: 'd' }); expect(setCanvas).toHaveBeenCalledWith('http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json'); }); describe('on-demand annotation fetching', () => { - it('fetches annotations', () => { - const requestCanvasAnnotations = jest.fn(); - const canvas = { - getHeight: () => 50, - getWidth: () => 50, - }; - wrapper = createWrapper({ annotationsCount: 0, canvas, requestCanvasAnnotations }); + const canvas = { + getHeight: () => 50, + getServices: jest.fn(), + getThumbnail: jest.fn(), + getType: jest.fn(), + getWidth: () => 50, + isCanvas: jest.fn(), + isCollection: jest.fn(), + isManifest: jest.fn(), + }; + let requestCanvasAnnotations; - wrapper.find(InView).simulate('change', { isIntersecting: true }); - expect(requestCanvasAnnotations).toHaveBeenCalled(); + beforeEach(() => { requestCanvasAnnotations = jest.fn(); }); + it('triggers requestCanvasAnnotations when there is an intersection and no annotions ', () => { + createWrapper({ annotationsCount: 0, canvas, requestCanvasAnnotations }); + mockAllIsIntersecting(true); + expect(requestCanvasAnnotations).toHaveBeenCalledTimes(1); }); - it('does nothing if there is no intersection', () => { - const requestCanvasAnnotations = jest.fn(); - const canvas = { - getHeight: () => 50, - getWidth: () => 50, - }; - wrapper = createWrapper({ canvas, requestCanvasAnnotations }); - - wrapper.find(InView).simulate('change', { isIntersecting: false }); + it('does nothing if there is an intersection and existing annotations', () => { + createWrapper({ annotationsCount: 1, canvas, requestCanvasAnnotations }); + mockAllIsIntersecting(true); expect(requestCanvasAnnotations).not.toHaveBeenCalled(); }); - it('does nothing if there are already some annotations', () => { - const requestCanvasAnnotations = jest.fn(); - const canvas = { - getHeight: () => 50, - getWidth: () => 50, - }; - wrapper = createWrapper({ annotationsCount: 5, canvas, requestCanvasAnnotations }); - - wrapper.find(InView).simulate('change', { isIntersecting: true }); + it('does nothing if there is no intersection', () => { + createWrapper({ annotationsCount: 0, canvas, requestCanvasAnnotations }); expect(requestCanvasAnnotations).not.toHaveBeenCalled(); }); }); describe('annotation count chip', () => { it('hides the chip if there are no annotations', () => { - wrapper = createWrapper({ annotationsCount: 0 }); - expect(wrapper.find(Chip).length).toEqual(0); + const { container } = createWrapper({ annotationsCount: 0 }); + expect(container.querySelector('.MuiChip-root')).not.toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access, testing-library/no-container }); it('shows the number of search annotations on a canvas', () => { - wrapper = createWrapper({ annotationsCount: 50 }); - expect(wrapper.find(Chip).length).toEqual(1); - expect(wrapper.find(Chip).prop('label')).toEqual(50); + const { container } = createWrapper({ annotationsCount: 50 }); + expect(container.querySelector('.MuiChip-root')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access, testing-library/no-container + expect(container.querySelector('.MuiChip-root')).toHaveTextContent('50'); // eslint-disable-line testing-library/no-node-access, testing-library/no-container }); }); describe('search annotation count chip', () => { it('hides the chip if there are no annotations', () => { - wrapper = createWrapper({ searchAnnotationsCount: 0 }); - expect(wrapper.find(Chip).length).toEqual(0); + const { container } = createWrapper({ searchAnnotationsCount: 0 }); + expect(container.querySelector('.MuiChip-root')).not.toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access, testing-library/no-container }); it('shows the number of search annotations on a canvas', () => { - wrapper = createWrapper({ searchAnnotationsCount: 50 }); - expect(wrapper.find(Chip).length).toEqual(1); - expect(wrapper.find(Chip).prop('label')).toEqual(50); + const { container } = createWrapper({ searchAnnotationsCount: 50 }); + expect(container.querySelector('.MuiChip-root')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access, testing-library/no-container + expect(container.querySelector('.MuiChip-root')).toHaveTextContent('50'); // eslint-disable-line testing-library/no-node-access, testing-library/no-container }); }); }); diff --git a/__tests__/src/components/IIIFAuthentication.test.js b/__tests__/src/components/IIIFAuthentication.test.js index c8f7261ff304aefe9de1a39fd105460d2acca086..d49d5e712d86dfd835d1353a816ed54910da433b 100644 --- a/__tests__/src/components/IIIFAuthentication.test.js +++ b/__tests__/src/components/IIIFAuthentication.test.js @@ -1,14 +1,12 @@ -import { shallow } from 'enzyme'; -import WindowAuthenticationBar from '../../../src/containers/WindowAuthenticationBar'; -import { NewWindow } from '../../../src/components/NewWindow'; -import { AccessTokenSender } from '../../../src/components/AccessTokenSender'; +import { screen, render, waitFor } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { IIIFAuthentication } from '../../../src/components/IIIFAuthentication'; /** - * Helper function to create a shallow wrapper around IIIFAuthentication + * Helper function to create IIIFAuthentication */ function createWrapper(props) { - return shallow( + return render( <IIIFAuthentication accessTokenServiceId="http://example.com/token" authServiceId="http://example.com/auth" @@ -28,73 +26,84 @@ function createWrapper(props) { } describe('IIIFAuthentication', () => { + let user; + beforeEach(() => { + user = userEvent.setup(); + }); describe('without an auth service', () => { it('renders nothing', () => { - const wrapper = createWrapper({ authServiceId: null }); - expect(wrapper.isEmptyRender()).toBe(true); + createWrapper({ authServiceId: null }); + expect(screen.queryByRole('button', { name: 'login' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); }); describe('with an available auth service', () => { - it('renders a login bar', () => { + it('renders a login bar', async () => { const handleAuthInteraction = jest.fn(); - const wrapper = createWrapper({ handleAuthInteraction }); - expect(wrapper.find(WindowAuthenticationBar).length).toBe(1); - expect(wrapper.find(WindowAuthenticationBar).simulate('confirm')); + createWrapper({ handleAuthInteraction }); + await user.click(screen.getByRole('button', { name: 'login' })); expect(handleAuthInteraction).toHaveBeenCalledWith('w', 'http://example.com/auth'); }); it('renders nothing for a non-interactive login', () => { - const wrapper = createWrapper({ isInteractive: false }); - expect(wrapper.isEmptyRender()).toBe(true); + createWrapper({ isInteractive: false }); + expect(screen.queryByText('login')).not.toBeInTheDocument(); }); }); describe('with a failed authentication', () => { - it('renders with an error message', () => { + it('renders with an error message', async () => { const handleAuthInteraction = jest.fn(); - const wrapper = createWrapper({ handleAuthInteraction, status: 'failed' }); - expect(wrapper.find(WindowAuthenticationBar).length).toBe(1); - expect(wrapper.find(WindowAuthenticationBar).prop('confirmButton')).toEqual('retry'); - expect(wrapper.find(WindowAuthenticationBar).prop('status')).toEqual('failed'); - expect(wrapper.find(WindowAuthenticationBar).prop('header')).toEqual('Login failed'); - expect(wrapper.find(WindowAuthenticationBar).prop('description')).toEqual('... and this is why.'); - expect(wrapper.find(WindowAuthenticationBar).simulate('confirm')); + createWrapper({ handleAuthInteraction, status: 'failed' }); + await user.click(screen.getByRole('button', { name: 'continue' })); + const confirmBtn = screen.getByRole('button', { name: /retry/ }); + expect(screen.getByText('Login failed')).toBeInTheDocument(); + expect(screen.getByText('cancel')).toBeInTheDocument(); + expect(screen.getByText('... and this is why.')).toBeInTheDocument(); + await user.click(confirmBtn); expect(handleAuthInteraction).toHaveBeenCalledWith('w', 'http://example.com/auth'); }); }); - describe('in the middle of authenicating', () => { - it('does the IIIF access cookie behavior', () => { - const wrapper = createWrapper({ status: 'cookie' }); - expect(wrapper.find(WindowAuthenticationBar).length).toBe(1); - expect(wrapper.find(NewWindow).length).toBe(1); - expect(wrapper.find(NewWindow).prop('url')).toContain('http://example.com/auth?origin='); + describe('in the middle of authenticating', () => { + it('does the IIIF access cookie behavior', async () => { + jest.useFakeTimers(); + const mockWindow = { close: jest.fn(), closed: false }; + const mockWindowOpen = jest.fn(() => (mockWindow)); + window.open = mockWindowOpen; + const resolveCookieMock = jest.fn(); + createWrapper({ resolveAuthenticationRequest: resolveCookieMock, status: 'cookie' }); + expect(screen.getByRole('button', { name: 'login' })).toBeInTheDocument(); + expect(mockWindowOpen).toHaveBeenCalledWith(`http://example.com/auth?origin=${window.origin}`, 'IiifLoginSender', 'centerscreen'); + mockWindow.closed = true; + jest.runOnlyPendingTimers(); + await waitFor(() => expect(resolveCookieMock).toHaveBeenCalledTimes(1)); + jest.useRealTimers(); }); - it('does the IIIF access token behavior', () => { - const wrapper = createWrapper({ status: 'token' }); - expect(wrapper.find(WindowAuthenticationBar).length).toBe(1); - expect(wrapper.find(AccessTokenSender).length).toBe(1); - expect(wrapper.find(AccessTokenSender).prop('url')).toEqual('http://example.com/token'); + it('does the IIIF access token behavior', async () => { + const resolveTokenMock = jest.fn(); + createWrapper({ resolveAccessTokenRequest: resolveTokenMock, status: 'token' }); + expect(screen.getByRole('button', { name: 'login' })).toBeInTheDocument(); + window.dispatchEvent(new MessageEvent('message', { + data: { messageId: 'http://example.com/token' }, + })); + await waitFor(() => expect(resolveTokenMock).toHaveBeenCalledWith('http://example.com/auth', 'http://example.com/token', { messageId: 'http://example.com/token' })); }); }); describe('when logged in', () => { - it('renders a logout button', () => { - const openWindow = jest.fn(); + it('renders a logout button', async () => { + const mockWindow = { open: jest.fn() }; + const mockWindowOpen = jest.fn(() => (mockWindow)); + window.open = mockWindowOpen; const resetAuthenticationState = jest.fn(); - const wrapper = createWrapper({ + createWrapper({ logoutConfirm: 'exit', - openWindow, + openWindow: mockWindowOpen, resetAuthenticationState, status: 'ok', }); - - expect(wrapper.find(WindowAuthenticationBar).length).toBe(1); - expect(wrapper.find(WindowAuthenticationBar).prop('confirmButton')).toEqual('exit'); - expect(wrapper.find(WindowAuthenticationBar).prop('hasLogoutService')).toEqual(true); - - wrapper.find(WindowAuthenticationBar).simulate('confirm'); - - expect(openWindow).toHaveBeenCalledWith('http://example.com/logout', undefined, 'centerscreen'); - expect(resetAuthenticationState).toHaveBeenCalledWith({ + const confirmBtn = await screen.findByRole('button', { name: 'exit' }); + await user.click(confirmBtn); + await waitFor(() => expect(resetAuthenticationState).toHaveBeenCalledWith({ authServiceId: 'http://example.com/auth', tokenServiceId: 'http://example.com/token', - }); + })); }); }); }); diff --git a/__tests__/src/components/IIIFIFrameCommunication.test.js b/__tests__/src/components/IIIFIFrameCommunication.test.js new file mode 100644 index 0000000000000000000000000000000000000000..d978b16a8268e76cc391d92126ba585fd0efca71 --- /dev/null +++ b/__tests__/src/components/IIIFIFrameCommunication.test.js @@ -0,0 +1,45 @@ +import { render, screen } from 'test-utils'; +import { IIIFIFrameCommunication } from '../../../src/components/IIIFIFrameCommunication'; + +/** */ +function createWrapper(props) { + render( + <IIIFIFrameCommunication + src="https://iiifauth.digtest.co.uk/auth/token/login/01_Icarus_Breughel.jpg?origin=http://localhost:4444&messageId=https://iiifauth.digtest.co.uk/auth/token/login/01_Icarus_Breughel.jpg" + title="AccessTokenSender" + handleReceiveMessage={() => {}} + {...props} + />, + ); +} + +describe('IIIFIFrameCommunication', () => { + it('should render an iframe', () => { + createWrapper(); + expect(screen.getByTitle('AccessTokenSender', { hidden: true })).toBeInTheDocument(); + }); +}); + +describe('Register event listener', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it('should call handleReceiveMessage on message event', () => { + const events = {}; + jest.spyOn(window, 'addEventListener').mockImplementation((event, onReceiveMessage) => { + events[event] = onReceiveMessage; + }); + jest.spyOn(window, 'removeEventListener').mockImplementation((event, onReceiveMessage) => { + events[event] = undefined; + }); + const props = { handleReceiveMessage: jest.fn() }; + const view = render(<IIIFIFrameCommunication {...props} />); + events.message(); + + expect(props.handleReceiveMessage).toBeCalledTimes(1); + expect(window.addEventListener).toBeCalledWith('message', expect.any(Function)); + + view.unmount(); + expect(window.removeEventListener).toBeCalledWith('message', expect.any(Function), false); + }); +}); diff --git a/__tests__/src/components/IIIFThumbnail.test.js b/__tests__/src/components/IIIFThumbnail.test.js index bc6ee94104f9cde6683ed3688d043cb9d143c8eb..1389dda33829211a0e0f3020fb11740470c4ee62 100644 --- a/__tests__/src/components/IIIFThumbnail.test.js +++ b/__tests__/src/components/IIIFThumbnail.test.js @@ -1,94 +1,86 @@ -import { shallow } from 'enzyme'; -import { InView } from 'react-intersection-observer'; -import Typography from '@material-ui/core/Typography'; +import { render, screen, act } from 'test-utils'; +import { mockAllIsIntersecting } from 'react-intersection-observer/test-utils'; import { IIIFThumbnail } from '../../../src/components/IIIFThumbnail'; /** * Helper function to create a shallow wrapper around IIIFThumbnail */ function createWrapper(props) { - return shallow( + return render( <IIIFThumbnail {...props} />, ); } +/* eslint-disable testing-library/no-node-access, testing-library/no-container */ describe('IIIFThumbnail', () => { - let wrapper; const url = 'http://example.com/iiif/image'; const thumbnail = { height: 120, url, width: 100 }; - beforeEach(() => { - wrapper = createWrapper({ thumbnail }); - }); - it('renders properly', () => { - expect(wrapper.matchesElement( - <div> - <InView onChange={wrapper.instance().handleIntersection}> - <img alt="" /> - </InView> - </div>, - )).toBe(true); + const { container } = createWrapper({ thumbnail }); + const img = container.querySelector('img'); + expect(img).toBeInTheDocument(); + expect(img).not.toHaveAccessibleName(); + expect(img).toHaveAttribute('src', expect.stringContaining('data:image')); }); it('renders a placeholder if there is no image', () => { - wrapper = createWrapper({}); - expect(wrapper.matchesElement( - <div> - <InView onChange={wrapper.instance().handleIntersection}> - <img alt="" /> - </InView> - </div>, - )).toBe(true); - expect(wrapper.find('img').props().src).toMatch(/data:image\/png;base64/); + const { container } = createWrapper({ thumbnail }); + const img = container.querySelector('img'); + expect(img).toHaveAttribute('src', expect.stringContaining('data:image')); }); - it('defaults using the placeholder image', () => { - expect(wrapper.find('img').props().src).toMatch(/data:image\/png;base64/); - }); + it('when handleIntersection is called, loads the image', async () => { + const { container } = createWrapper({ thumbnail }); + const img = container.querySelector('img'); + + act(() => { + mockAllIsIntersecting(true); + }); - it('when handleIntersection is called, loads the image', () => { - wrapper.instance().handleIntersection({ isIntersecting: true }); - expect(wrapper.find('img').props().src).toEqual(url); + expect(img).toHaveAttribute('src', url); }); it('can be constrained by maxHeight', () => { - wrapper = createWrapper({ maxHeight: 100, thumbnail }); + const { container } = createWrapper({ maxHeight: 100, thumbnail }); + const img = container.querySelector('img'); - expect(wrapper.find('img').props().style).toMatchObject({ height: 100, width: 'auto' }); + expect(img).toHaveStyle({ height: '100px', width: 'auto' }); }); it('can be constrained by maxWidth', () => { - wrapper = createWrapper({ maxWidth: 80, thumbnail }); + const { container } = createWrapper({ maxWidth: 80, thumbnail }); + const img = container.querySelector('img'); - expect(wrapper.find('img').props().style).toMatchObject({ height: 'auto', width: 80 }); + expect(img).toHaveStyle({ height: 'auto', width: '80px' }); }); it('can be constrained by maxWidth and maxHeight', () => { - wrapper = createWrapper({ maxHeight: 90, maxWidth: 50, thumbnail }); + const { container } = createWrapper({ maxHeight: 90, maxWidth: 50, thumbnail }); + const img = container.querySelector('img'); - expect(wrapper.find('img').props().style).toMatchObject({ height: 60, width: 50 }); + expect(img).toHaveStyle({ height: '60px', width: '50px' }); }); it('constrains what it can when the image dimensions are unknown', () => { - wrapper = createWrapper({ maxHeight: 90, thumbnail: { height: 120, url } }); - expect(wrapper.find('img').props().style).toMatchObject({ height: 90, width: 'auto' }); + const { container } = createWrapper({ maxHeight: 90, thumbnail: { height: 120, url } }); + const img = container.querySelector('img'); + + expect(img).toHaveStyle({ height: '90px', width: 'auto' }); }); it('renders a provided label', () => { - wrapper = createWrapper({ + createWrapper({ classes: { label: 'label' }, label: 'Some label', labelled: true, thumbnail, }); - expect( - wrapper.find('div.label').at(0).matchesElement( - <div className="label"><Typography>Some label</Typography></div>, - ), - ).toBe(true); + + expect(screen.getByText('Some label')).toBeInTheDocument(); }); it('renders children', () => { - wrapper = createWrapper({ children: <span id="hi" />, thumbnail }); - expect(wrapper.find('span').length).toEqual(1); + createWrapper({ children: <span data-testid="hi" />, thumbnail }); + + expect(screen.getByTestId('hi')).toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/LabelValueMetadata.test.js b/__tests__/src/components/LabelValueMetadata.test.js index a53a3f01c7d42ab98e9b435d88555ea3727a9323..06b46800471e29c4f8f98e1bfc81e12a73ea69b8 100644 --- a/__tests__/src/components/LabelValueMetadata.test.js +++ b/__tests__/src/components/LabelValueMetadata.test.js @@ -1,8 +1,7 @@ -import { shallow } from 'enzyme'; -import Typography from '@material-ui/core/Typography'; +import { render, screen } from 'test-utils'; import { LabelValueMetadata } from '../../../src/components/LabelValueMetadata'; -import SanitizedHtml from '../../../src/containers/SanitizedHtml'; +/* eslint-disable testing-library/no-node-access */ describe('LabelValueMetadata', () => { let wrapper; let labelValuePair; @@ -19,46 +18,38 @@ describe('LabelValueMetadata', () => { values: ['Value 2'], }, ]; - wrapper = shallow( + wrapper = render( <LabelValueMetadata labelValuePairs={labelValuePair} />, ); }); it('renders a dt/dd for each label/value pair', () => { - expect(wrapper.find('dl').length).toEqual(1); - expect(wrapper.find(Typography).find('[component="dt"]').length).toEqual(2); - expect(wrapper.find(Typography).find('[component="dd"]').length).toEqual(2); + expect(wrapper.container.querySelector('dl')).toBeInTheDocument(); + expect(wrapper.container.querySelectorAll('dt').length).toEqual(2); + expect(wrapper.container.querySelectorAll('dd').length).toEqual(2); }); it('renders correct labels in dt', () => { - expect(wrapper.find(Typography).find('[component="dt"]').first().children() - .text()).toEqual('Label 1'); - expect(wrapper.find(Typography).find('[component="dt"]').last().children() - .text()).toEqual('Label 2'); + expect(screen.getByText('Label 1')).toBeInTheDocument(); + expect(screen.getByText('Label 2')).toBeInTheDocument(); }); - it('renders SanitizedHtml component in dt for each value', () => { - expect(wrapper.find(Typography).find('[component="dd"]').first().find(SanitizedHtml).length).toBe(1); - expect(wrapper.find(Typography).find('[component="dd"]').last().find(SanitizedHtml).length).toBe(1); - }); - - it('passes value string to SanitizedHtml', () => { - expect(wrapper.find(SanitizedHtml).first().props().htmlString).toBe('Value 1'); - expect(wrapper.find(SanitizedHtml).last().props().htmlString).toBe('Value 2'); + it('renders SanitizedHtml component in dd for each value', () => { + expect(screen.getByText('Value 1')).toBeInTheDocument(); + expect(screen.getByText('Value 2')).toBeInTheDocument(); }); }); describe('when the labelValuePair has no content', () => { beforeEach(() => { labelValuePair = []; - wrapper = shallow( + wrapper = render( <LabelValueMetadata labelValuePairs={labelValuePair} />, ); }); it('renders an empty fragment instead of an empty dl', () => { - expect(wrapper.find('dl').length).toEqual(0); - expect(wrapper.isEmptyRender()).toBe(true); + expect(wrapper.container).toBeEmptyDOMElement(); }); }); @@ -73,16 +64,14 @@ describe('LabelValueMetadata', () => { values: ['Value 2'], }, ]; - wrapper = shallow( + wrapper = render( <LabelValueMetadata labelValuePairs={labelValuePair} defaultLabel="Default label" />, ); }); it('renders correct labels in dt', () => { - expect(wrapper.find(Typography).find('[component="dt"]').first().children() - .text()).toEqual('Default label'); - expect(wrapper.find(Typography).find('[component="dt"]').last().children() - .text()).toEqual('Label 2'); + expect(screen.getByText('Default label')).toBeInTheDocument(); + expect(screen.getByText('Label 2')).toBeInTheDocument(); }); }); }); diff --git a/__tests__/src/components/LanguageSettings.test.js b/__tests__/src/components/LanguageSettings.test.js index e4e5462cc1366eeea37887993948bf0359fb9682..98b0a82f6d7bce81ff7767993531b8377eef9b50 100644 --- a/__tests__/src/components/LanguageSettings.test.js +++ b/__tests__/src/components/LanguageSettings.test.js @@ -1,14 +1,12 @@ -import { shallow } from 'enzyme'; -import ListItemText from '@material-ui/core/ListItemText'; -import MenuItem from '@material-ui/core/MenuItem'; -import CheckIcon from '@material-ui/icons/CheckSharp'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { LanguageSettings } from '../../../src/components/LanguageSettings'; /** * Helper function to create a shallow wrapper around LanguageSettings */ function createWrapper(props) { - return shallow( + return render( <LanguageSettings handleClick={() => {}} languages={{}} @@ -18,7 +16,6 @@ function createWrapper(props) { } describe('LanguageSettings', () => { - let wrapper; const languages = [ { current: true, @@ -33,62 +30,33 @@ describe('LanguageSettings', () => { ]; it('renders a list with a list item for each language passed in props', () => { - wrapper = createWrapper({ languages }); + createWrapper({ languages }); - expect(wrapper.find(MenuItem).length).toBe(2); - }); - - it('non-active list items are buttons (and active are not)', () => { - wrapper = createWrapper({ languages }); - - expect( - wrapper - .find(MenuItem) - .first() // The German / active button - .prop('button'), - ).toBe(false); - - expect( - wrapper - .find(MenuItem) - .last() // The English / non-active button - .prop('button'), - ).toBe(true); + expect(screen.getAllByRole('menuitem')).toHaveLength(2); }); it('renders the check icon when the active prop returns true', () => { - wrapper = createWrapper({ languages }); + createWrapper({ languages }); - expect( - wrapper - .find(MenuItem) - .first() - .find(CheckIcon) - .length, - ).toBe(1); + expect(screen.getByRole('menuitem', { name: 'Deutsch' }).querySelector('svg')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access + expect(screen.getByRole('menuitem', { name: 'English' }).querySelector('svg')).not.toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access, testing-library/prefer-presence-queries }); it('renders the language value in an Typography element wrapped in a ListItemText', () => { - wrapper = createWrapper({ languages }); - - const firstListText = wrapper - .find(MenuItem) - .first() - .find(ListItemText) - .children() - .text(); + createWrapper({ languages }); - expect(firstListText).toEqual('Deutsch'); + expect(screen.getByText('Deutsch')).toHaveClass('MuiTypography-body1'); }); - it('triggers the handleClick prop when clicking a list item', () => { + it('triggers the handleClick prop when clicking a list item', async () => { + const user = userEvent.setup(); const mockHandleClick = jest.fn(); - wrapper = createWrapper({ + createWrapper({ handleClick: mockHandleClick, languages, }); - wrapper.find(MenuItem).last().simulate('click'); + await user.click(screen.getByRole('menuitem', { name: 'English' })); expect(mockHandleClick).toHaveBeenCalledTimes(1); expect(mockHandleClick).toHaveBeenCalledWith('en'); diff --git a/__tests__/src/components/LayersPanel.test.js b/__tests__/src/components/LayersPanel.test.js index 9439ae81b486ffd6f3b770b023501699bae54c32..82eb293e86b6574e09036a6f0771c7b7d5e8cf11 100644 --- a/__tests__/src/components/LayersPanel.test.js +++ b/__tests__/src/components/LayersPanel.test.js @@ -1,39 +1,28 @@ -import { shallow } from 'enzyme'; -import CanvasLayers from '../../../src/containers/CanvasLayers'; +import { render, screen } from 'test-utils'; + import { LayersPanel } from '../../../src/components/LayersPanel'; /** * Helper function to create a shallow wrapper around AttributionPanel */ function createWrapper(props) { - return shallow( + return render( <LayersPanel id="xyz" t={str => str} windowId="window" {...props} />, + { preloadedState: { companionWindows: { xyz: { content: 'layers' } } } }, ); } describe('LayersPanel', () => { it('renders layers for each canvas', () => { const canvasIds = ['a', 'b']; - const wrapper = createWrapper({ canvasIds }); - expect(wrapper.find(CanvasLayers).length).toBe(2); - - expect(wrapper.find(CanvasLayers).at(0).props()).toMatchObject({ - canvasId: 'a', - index: 0, - totalSize: 2, - windowId: 'window', - }); + createWrapper({ canvasIds }); - expect(wrapper.find(CanvasLayers).at(1).props()).toMatchObject({ - canvasId: 'b', - index: 1, - totalSize: 2, - windowId: 'window', - }); + expect(screen.getAllByText('annotationCanvasLabel').length).toBe(2); + expect(screen.getAllByRole('list').length).toBe(2); }); }); diff --git a/__tests__/src/components/LocalePicker.test.js b/__tests__/src/components/LocalePicker.test.js index 4babd052f1e381a40b2b3090486e6c25848029c0..2ff1b2ceefa4c4e1f96a2a06ee1ead0476de8e85 100644 --- a/__tests__/src/components/LocalePicker.test.js +++ b/__tests__/src/components/LocalePicker.test.js @@ -1,13 +1,12 @@ -import { shallow } from 'enzyme'; -import MenuItem from '@material-ui/core/MenuItem'; -import Select from '@material-ui/core/Select'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { LocalePicker } from '../../../src/components/LocalePicker'; /** - * Helper function to create a shallow wrapper around LanguageSettings + * Helper function to create a shallow wrapper around LocalePicker */ function createWrapper(props) { - return shallow( + return render( <LocalePicker availableLocales={[]} locale={undefined} @@ -18,36 +17,49 @@ function createWrapper(props) { } describe('LocalePicker', () => { - let wrapper; - it('hides the control if there are not locales to switch to', () => { - wrapper = createWrapper({ availableLocales: ['en'] }); + const { container } = createWrapper({ availableLocales: ['en'] }); - expect(wrapper.find(Select).length).toBe(0); + expect(container).toBeEmptyDOMElement(); // eslint-disable-line testing-library/no-container }); it('renders a select with the current value', () => { - wrapper = createWrapper({ availableLocales: ['en', 'de'], locale: 'en' }); - - expect(wrapper.find(Select).length).toBe(1); - expect(wrapper.find(Select).props().value).toBe('en'); + createWrapper({ availableLocales: ['en', 'de'], locale: 'de' }); + // The option to expand the dropdown menu is rendered by a CompanionWindow titleControls prop in WindowSideBarInfoPanel, which is a combobox + const dropdownTitle = screen.getByRole('combobox'); + expect(dropdownTitle).toHaveTextContent('de'); }); - it('renders a select with a list item for each language passed in props', () => { - wrapper = createWrapper({ availableLocales: ['en', 'de'] }); - - expect(wrapper.find(MenuItem).length).toBe(2); + it('renders a select with both options and sets the current value', async () => { + const user = userEvent.setup(); + createWrapper({ availableLocales: ['en', 'de'], locale: 'de' }); + const dropdownTitle = screen.getByRole('combobox'); + // Open the menu + await user.click(dropdownTitle); + // The dropddown menu is not nested within the combobox, it is a sibling in the DOM, an MuiMenu + const menu = screen.getByRole('listbox'); + // Assert that the menu element has 2 children (2 options) + expect(menu.children).toHaveLength(2); // eslint-disable-line testing-library/no-node-access + // Verify that the select element has the correct value ('de') + const deOption = screen.getByRole('option', { name: 'de' }); + expect(deOption).toHaveAttribute('aria-selected', 'true'); + // Verify en is also an option + expect(screen.getByRole('option', { name: 'en' })).toBeInTheDocument(); }); - it('triggers setLocale prop when clicking a list item', () => { + it('triggers setLocale prop when clicking a list item', async () => { + const user = userEvent.setup(); const setLocale = jest.fn(); - - wrapper = createWrapper({ + createWrapper({ availableLocales: ['en', 'de'], + locale: 'en', setLocale, }); - wrapper.find(Select).simulate('change', { target: { value: 'de' } }); - + const dropdownTitle = screen.getByRole('combobox'); + // Open the Select component + await user.click(dropdownTitle); + // Change the locale to 'de' + await user.click(screen.getByRole('option', { name: 'de' })); expect(setLocale).toHaveBeenCalledTimes(1); expect(setLocale).toHaveBeenCalledWith('de'); }); diff --git a/__tests__/src/components/ManifestForm.test.js b/__tests__/src/components/ManifestForm.test.js index 543aa66d7c48056471f86f9964e77920cba3addf..edb81fa6eec599ffa10130cf2859ed23cb9e282c 100644 --- a/__tests__/src/components/ManifestForm.test.js +++ b/__tests__/src/components/ManifestForm.test.js @@ -1,9 +1,10 @@ -import { mount } from 'enzyme'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { ManifestForm } from '../../../src/components/ManifestForm'; /** create wrapper */ function createWrapper(props) { - return mount( + return render( <ManifestForm addResource={() => {}} t={str => str} @@ -14,40 +15,41 @@ function createWrapper(props) { describe('ManifestForm', () => { it('renders nothing if it is not open', () => { - const wrapper = createWrapper({ addResourcesOpen: false }); - expect(wrapper.find('ForwardRef(TextField)[label="addManifestUrl"]').length).toBe(0); + createWrapper({ addResourcesOpen: false }); + + expect(screen.queryByRole('textbox', { name: 'addManifestUrl' })).not.toBeInTheDocument(); }); it('renders the form fields', () => { - const wrapper = createWrapper({ addResourcesOpen: true }); - expect(wrapper.find('ForwardRef(TextField)[label="addManifestUrl"]').length).toBe(1); - expect(wrapper.find('button[type="submit"]').length).toBe(1); + createWrapper({ addResourcesOpen: true }); + + expect(screen.getByRole('textbox', { name: 'addManifestUrl' })).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveAttribute('type', 'submit'); }); - it('has a cancel button when a cancel action is provided', () => { + it('has a cancel button when a cancel action is provided', async () => { + const user = userEvent.setup(); const onCancel = jest.fn(); - const wrapper = createWrapper({ addResourcesOpen: true, onCancel }); - wrapper.setState({ formValue: 'asdf' }); + createWrapper({ addResourcesOpen: true, onCancel }); - expect(wrapper.find('button[onClick]').length).toBe(1); + await user.type(screen.getByRole('textbox', { name: 'addManifestUrl' }), 'asdf'); - wrapper.find('button[onClick]').simulate('click'); + await user.click(screen.getByRole('button', { name: 'cancel' })); expect(onCancel).toHaveBeenCalled(); - expect(wrapper.state().formValue).toBe(''); + expect(screen.getByRole('textbox')).toHaveValue(''); }); - it('triggers an action when the form is submitted', () => { + it('triggers an action when the form is submitted', async () => { + const user = userEvent.setup(); const addResource = jest.fn(); const onSubmit = jest.fn(); - const wrapper = createWrapper({ addResource, addResourcesOpen: true, onSubmit }); - wrapper.setState({ formValue: 'asdf' }); - - wrapper.setState({ formValue: 'http://example.com/iiif' }); + createWrapper({ addResource, addResourcesOpen: true, onSubmit }); + await user.type(screen.getByRole('textbox', { name: 'addManifestUrl' }), 'http://example.com/iiif'); + await user.click(screen.getByRole('button', { name: 'fetchManifest' })); - wrapper.find('form').simulate('submit', { preventDefault: () => {} }); expect(addResource).toHaveBeenCalledWith('http://example.com/iiif'); expect(onSubmit).toHaveBeenCalled(); - expect(wrapper.state().formValue).toBe(''); + expect(screen.getByRole('textbox')).toHaveValue(''); }); }); diff --git a/__tests__/src/components/ManifestInfo.test.js b/__tests__/src/components/ManifestInfo.test.js index eda171cca4b2baaa1c7e1b02b76461135adc9dd7..46e90bcc098e2b27d01572833ca6ad386e49748c 100644 --- a/__tests__/src/components/ManifestInfo.test.js +++ b/__tests__/src/components/ManifestInfo.test.js @@ -1,67 +1,54 @@ -import { shallow } from 'enzyme'; -import Typography from '@material-ui/core/Typography'; +import { render, screen } from 'test-utils'; import { ManifestInfo } from '../../../src/components/ManifestInfo'; -import { LabelValueMetadata } from '../../../src/components/LabelValueMetadata'; -import CollapsibleSection from '../../../src/containers/CollapsibleSection'; -import SanitizedHtml from '../../../src/containers/SanitizedHtml'; describe('ManifestInfo', () => { - const metadata = [{ label: {}, value: {} }]; - let wrapper; + const metadata = [{ label: 'some label', values: ['some value'] }]; describe('when metadata is present', () => { beforeEach(() => { - wrapper = shallow( + render( <ManifestInfo id="xyz" manifestLabel="The Manifest Label" manifestDescription="The Manifest Description" manifestMetadata={metadata} + manifestSummary="The Manifest Summary" t={str => str} />, ); }); it('renders the content in a CollapsibleSection', () => { - expect(wrapper.find(CollapsibleSection).length).toBe(1); + expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('resource'); }); it('renders manifest label', () => { - expect( - wrapper.find(Typography).at(0).matchesElement( - <Typography>The Manifest Label</Typography>, - ), - ).toBe(true); + expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent('The Manifest Label'); }); it('renders manifest description in SanitizedHtml component', () => { - expect( - wrapper.find(Typography).at(1).matchesElement( - <Typography> - <SanitizedHtml htmlString="The Manifest Description" ruleSet="iiif" /> - </Typography>, - ), - ).toBe(true); + expect(screen.getByText('The Manifest Description')).toBeInTheDocument(); + }); + + it('renders manifest summary in SanitizedHtml component', () => { + expect(screen.getByText('The Manifest Summary')).toBeInTheDocument(); }); it('renders manifest metadata in LabelValueMetadata component', () => { - expect( - wrapper.find(LabelValueMetadata).at(0).matchesElement( - <LabelValueMetadata labelValuePairs={metadata} />, - ), - ).toBe(true); + expect(screen.getByText('some label')).toBeInTheDocument(); + expect(screen.getByText('some value')).toBeInTheDocument(); }); }); describe('when metadata is not present', () => { beforeEach(() => { - wrapper = shallow( + render( <ManifestInfo id="xyz" />, ); }); it('does not render empty elements elements', () => { - expect(wrapper.find(LabelValueMetadata).length).toBe(0); + expect(screen.queryByRole('heading', { level: 5 })).not.toBeInTheDocument(); }); }); }); diff --git a/__tests__/src/components/ManifestListItem.test.js b/__tests__/src/components/ManifestListItem.test.js index 3b80d03a23df4a22a62ecfb2ebfe58566062a13a..bd444de9156ad4b77d10e828de451631c19bb105 100644 --- a/__tests__/src/components/ManifestListItem.test.js +++ b/__tests__/src/components/ManifestListItem.test.js @@ -1,13 +1,11 @@ -import { shallow } from 'enzyme'; -import ButtonBase from '@material-ui/core/ButtonBase'; -import ListItem from '@material-ui/core/ListItem'; -import Typography from '@material-ui/core/Typography'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; + import { ManifestListItem } from '../../../src/components/ManifestListItem'; -import ManifestListItemError from '../../../src/containers/ManifestListItemError'; /** */ function createWrapper(props) { - return shallow( + return render( <ManifestListItem manifestId="http://example.com" title="xyz" @@ -22,53 +20,54 @@ function createWrapper(props) { describe('ManifestListItem', () => { it('renders without an error', () => { - const wrapper = createWrapper({ buttonRef: 'ref' }); - expect(wrapper.find('.mirador-manifest-list-item').length).toBe(1); - expect(wrapper.find(ButtonBase).length).toBe(1); - expect(wrapper.find(ButtonBase).getElement().ref).toBe('ref'); - expect(wrapper.find(ButtonBase).find(Typography).children().text()).toEqual('xyz'); + createWrapper({ buttonRef: 'ref' }); + + expect(screen.getByRole('listitem')).toHaveAttribute('data-manifestid', 'http://example.com'); + expect(screen.getByRole('listitem')).toHaveClass('MuiListItem-root'); + expect(screen.getByRole('button')).toHaveTextContent('xyz'); }); it('adds a class when the item is active', () => { - const wrapper = createWrapper({ active: true, classes: { active: 'active' } }); - expect(wrapper.find('.active').length).toEqual(1); + createWrapper({ active: true, classes: { active: 'active' } }); + + // If this is true, we can assume the proper styling classes are being applied + expect(screen.getByRole('listitem')).toHaveAttribute('data-active', 'true'); + + expect(screen.getByRole('listitem')).toHaveClass('active'); + expect(screen.getByRole('listitem')).toHaveClass('Mui-selected'); }); it('renders a placeholder element until real data is available', () => { - const wrapper = createWrapper({ ready: false }); + const { container } = createWrapper({ ready: false }); - expect(wrapper.find('.mirador-manifest-list-item').length).toBe(1); - expect(wrapper.find('WithStyles(ForwardRef(Skeleton))').length > 0).toBe(true); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + expect(container.querySelectorAll('.MuiSkeleton-rectangular').length).toBeGreaterThan(0); // eslint-disable-line testing-library/no-node-access, testing-library/no-container }); it('renders an error message if fetching the manifest failed', () => { - const wrapper = createWrapper({ error: 'This is an error message' }); + createWrapper({ error: 'This is an error message' }); - expect(wrapper.find(ListItem).length).toBe(1); - expect(wrapper.find(ManifestListItemError).length).toBe(1); + expect(screen.getByText('manifestError')).toBeInTheDocument(); + expect(screen.getByText('http://example.com')).toBeInTheDocument(); }); - it('updates and adds window when button clicked', () => { + it('updates and adds window when button clicked', async () => { + const user = userEvent.setup(); const addWindow = jest.fn(); - const wrapper = createWrapper({ addWindow }); - wrapper.find(ButtonBase).simulate('click'); + createWrapper({ addWindow }); + + await user.click(screen.getByRole('button')); expect(addWindow).toHaveBeenCalledTimes(1); }); it('uses the manifest id if the title is not available', () => { - const wrapper = createWrapper({ ready: true, title: null }); - - expect(wrapper.find(ButtonBase).length).toBe(1); - expect(wrapper.find(ButtonBase).find(Typography).children().text()).toEqual('http://example.com'); + createWrapper({ ready: true, title: null }); + expect(screen.getByRole('button')).toHaveTextContent('http://example.com'); }); it('displays the provider information', () => { - const wrapper = createWrapper({ provider: 'ACME' }); - expect(wrapper.find('.mirador-manifest-list-item-provider').children().text()).toEqual('ACME'); - }); - - it('displays nothing if no information is given', () => { - const wrapper = createWrapper(); - expect(wrapper.find('.mirador-manifest-list-item-provider').children().length).toEqual(0); + createWrapper({ provider: 'ACME' }); + expect(screen.getByText('ACME', { container: '.mirador-manifest-list-item-provider' })).toHaveTextContent('ACME'); }); it('displays a collection label for collections', () => { - const wrapper = createWrapper({ isCollection: true }); - expect(wrapper.text()).toContain('collectionxyz'); + createWrapper({ isCollection: true }); + + expect(screen.getByText('xyz', { container: '.MuiTypography-h6' })).toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/ManifestListItemError.test.js b/__tests__/src/components/ManifestListItemError.test.js index c70ec7948cb1759bcd3dcb23ef217940bb0252bb..aef9880b7a8bd0bc6e0275a2cee0fe448d2b8a11 100644 --- a/__tests__/src/components/ManifestListItemError.test.js +++ b/__tests__/src/components/ManifestListItemError.test.js @@ -1,13 +1,12 @@ -import { shallow } from 'enzyme'; -import Button from '@material-ui/core/Button'; -import Typography from '@material-ui/core/Typography'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { ManifestListItemError } from '../../../src/components/ManifestListItemError'; /** * Helper function to wrap creating a ManifestListItemError component */ function createWrapper(props) { - return shallow( + return render( <ManifestListItemError classes={{}} manifestId="http://example.com" @@ -20,36 +19,32 @@ function createWrapper(props) { } describe('ManifestListItemError', () => { - let wrapper; let mockFn; it('renders the failed manifest url and error key', () => { - wrapper = createWrapper(); + createWrapper(); - expect( - wrapper.find(Typography).children().first().text(), - ).toEqual('manifestError'); // the i18n key - - expect( - wrapper.find(Typography).children().last().text(), - ).toEqual('http://example.com'); + expect(screen.getByText('manifestError')).toBeInTheDocument(); + expect(screen.getByText('http://example.com')).toBeInTheDocument(); }); - it('has a dismiss button that fires the onDismissClick prop', () => { + it('has a dismiss button that fires the onDismissClick prop', async () => { + const user = userEvent.setup(); mockFn = jest.fn(); - wrapper = createWrapper({ onDismissClick: mockFn }); + createWrapper({ onDismissClick: mockFn }); + + await user.click(screen.getByRole('button', { name: 'dismiss' })); - wrapper.find(Button).first().simulate('click'); - expect(mockFn).toHaveBeenCalledTimes(1); expect(mockFn).toHaveBeenCalledWith('http://example.com'); }); - it('has a try again button that fires the onTryAgainClick prop', () => { + it('has a try again button that fires the onTryAgainClick prop', async () => { + const user = userEvent.setup(); mockFn = jest.fn(); - wrapper = createWrapper({ onTryAgainClick: mockFn }); + createWrapper({ onTryAgainClick: mockFn }); + + await user.click(screen.getByRole('button', { name: 'tryAgain' })); - wrapper.find(Button).last().simulate('click'); - expect(mockFn).toHaveBeenCalledTimes(1); expect(mockFn).toHaveBeenCalledWith('http://example.com'); }); }); diff --git a/__tests__/src/components/ManifestRelatedLinks.test.js b/__tests__/src/components/ManifestRelatedLinks.test.js index 270608bff950163aefe0320ac79ecbbccc7d7924..fe07eae5c8abedd81410e348fe4ad70b7507aa02 100644 --- a/__tests__/src/components/ManifestRelatedLinks.test.js +++ b/__tests__/src/components/ManifestRelatedLinks.test.js @@ -1,15 +1,11 @@ -import { shallow } from 'enzyme'; -import Typography from '@material-ui/core/Typography'; -import Link from '@material-ui/core/Link'; -import CollapsibleSection from '../../../src/containers/CollapsibleSection'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { ManifestRelatedLinks } from '../../../src/components/ManifestRelatedLinks'; describe('ManifestRelatedLinks', () => { - let wrapper; - describe('when metadata is present', () => { beforeEach(() => { - wrapper = shallow( + render( <ManifestRelatedLinks classes={{}} id="xyz" @@ -52,110 +48,44 @@ describe('ManifestRelatedLinks', () => { ); }); - it('renders the content in a CollapsibleSection', () => { - expect(wrapper.find(CollapsibleSection).length).toBe(1); + it('renders the content in a CollapsibleSection', async () => { + const user = userEvent.setup(); + expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('related'); + expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent(/links/); + + await user.click(screen.getByRole('button', { name: 'collapseSection' })); + + expect(screen.queryByRole('heading', { level: 5 })).not.toBeInTheDocument(); }); it('renders manifest homepage information', () => { - expect( - wrapper.find(Typography).at(1) - .matchesElement( - <Typography component="dt">iiif_homepage</Typography>, - ), - ).toBe(true); + expect(screen.getByText('iiif_homepage').tagName).toEqual('DT'); - expect( - wrapper.find(Typography).at(2) - .matchesElement( - <Typography component="dd"><Link href="http://example.com/">Home page</Link></Typography>, - ), - ).toBe(true); + expect(screen.getByRole('link', { name: 'Home page' })).toHaveAttribute('href', 'http://example.com/'); }); it('renders manifest renderings information', () => { - expect( - wrapper.find(Typography).at(3) - .matchesElement( - <Typography component="dt">iiif_renderings</Typography>, - ), - ).toBe(true); - - expect( - wrapper.find(Typography).at(4) - .matchesElement( - <Typography component="dd"> - <Link href="http://example.com/pdf">PDF Version</Link> - </Typography>, - ), - ).toBe(true); + expect(screen.getByText('iiif_renderings').tagName).toEqual('DT'); + expect(screen.getByRole('link', { name: 'PDF Version' })).toHaveAttribute('href', 'http://example.com/pdf'); }); it('renders related information', () => { - expect( - wrapper.find(Typography).at(5) - .matchesElement( - <Typography component="dt">iiif_related</Typography>, - ), - ).toBe(true); - - expect( - wrapper.find(Typography).at(6) - .matchesElement( - <Typography component="dd"><Link href="http://example.com/related">http://example.com/related</Link></Typography>, - ), - ).toBe(true); - - expect( - wrapper.find(Typography).at(7) - .matchesElement( - <Typography component="dd"> - <Link href="http://example.com/video">Video</Link> - <Typography>(video/ogg)</Typography> - </Typography>, - ), - ).toBe(true); + expect(screen.getByText('iiif_related').tagName).toEqual('DT'); + expect(screen.getByRole('link', { name: 'http://example.com/related' })).toHaveAttribute('href', 'http://example.com/related'); + expect(screen.getByRole('link', { name: 'Video' })).toHaveAttribute('href', 'http://example.com/video'); + expect(screen.getByText('(video/ogg)')).toBeInTheDocument(); }); it('renders manifest seeAlso information', () => { - expect( - wrapper.find(Typography).at(9) - .matchesElement( - <Typography component="dt">iiif_seeAlso</Typography>, - ), - ).toBe(true); - - expect( - wrapper.find(Typography).at(10) - .matchesElement( - <Typography component="dd"> - <Link href="http://example.com/a">A</Link> - <Typography>(text/html)</Typography> - </Typography>, - ), - ).toBe(true); - - expect( - wrapper.find(Typography).at(12) - .matchesElement( - <Typography component="dd"><Link href="http://example.com/b">http://example.com/b</Link></Typography>, - ), - ).toBe(true); + expect(screen.getByText('iiif_seeAlso').tagName).toEqual('DT'); + expect(screen.getByRole('link', { name: 'A' })).toHaveAttribute('href', 'http://example.com/a'); + expect(screen.getByText('(text/html)')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'http://example.com/b' })).toHaveAttribute('href', 'http://example.com/b'); }); it('renders manifest links', () => { - expect( - wrapper.find(Typography).at(13) - .matchesElement( - <Typography component="dt">iiif_manifest</Typography>, - ), - ).toBe(true); - - expect( - wrapper.find(Typography).at(14) - .matchesElement( - <Typography component="dd"><Link href="http://example.com/">http://example.com/</Link></Typography>, - ), - ).toBe(true); + expect(screen.getByText('iiif_manifest').tagName).toEqual('DT'); + expect(screen.getByRole('link', { name: 'http://example.com/' })).toHaveAttribute('href', 'http://example.com/'); }); }); }); diff --git a/__tests__/src/components/MiradorMenuButton.test.js b/__tests__/src/components/MiradorMenuButton.test.js index 4e3783f71003bdc672448d61133a014d979332fc..6ac20aa319e28a1a551aad37bf5f3f1f3ac944c3 100644 --- a/__tests__/src/components/MiradorMenuButton.test.js +++ b/__tests__/src/components/MiradorMenuButton.test.js @@ -1,66 +1,60 @@ -import { shallow } from 'enzyme'; -import Badge from '@material-ui/core/Badge'; -import IconButton from '@material-ui/core/IconButton'; -import Tooltip from '@material-ui/core/Tooltip'; +import { act, render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { MiradorMenuButton } from '../../../src/components/MiradorMenuButton'; /** * Helper function to wrap creating a MiradorMenuButton component */ function createWrapper(props) { - return shallow( - <MiradorMenuButton aria-label="The Label" containerId="mirador" {...props}> + return render( + <MiradorMenuButton aria-label="The Label" {...props}> icon </MiradorMenuButton>, ); } describe('MiradorMenuButton', () => { - let wrapper; + it('renders the given a Tooltip -> IconLabel -> Icon', async () => { + const user = userEvent.setup(); - it('renders the given a Tooltip -> IconLabel -> Icon', () => { - wrapper = createWrapper(); + createWrapper(); - expect(wrapper.find(Tooltip).find(IconButton).length).toEqual(1); - expect( - wrapper - .find(Tooltip).find(IconButton) - .first() - .children() - .text(), - ).toEqual('icon'); - }); + expect(screen.getByRole('button')).toHaveAccessibleName('The Label'); + expect(screen.getByRole('button')).toHaveTextContent('icon'); - it('does not render the Tooltip if the button is disabled', () => { - wrapper = createWrapper({ disabled: true }); + await user.hover(screen.getByRole('button')); - expect(wrapper.find(Tooltip).find(IconButton).length).toEqual(0); - expect(wrapper.find(IconButton).length).toEqual(1); + expect(await screen.findByRole('tooltip')).toHaveTextContent('The Label'); }); - it('uses the aria-label prop the the Tooltip title prop', () => { - wrapper = createWrapper(); + it('does not render the Tooltip if the button is disabled', () => { + createWrapper({ disabled: true }); - expect(wrapper.find(Tooltip).props().title).toEqual('The Label'); - expect(wrapper.find(Tooltip).find(IconButton).props()['aria-label']).toEqual('The Label'); + expect(screen.getByRole('button')).toBeDisabled(); }); - it('spreads TooltipProps to the Tooltip component', () => { - wrapper = createWrapper({ TooltipProps: { style: { color: 'red' } } }); + it('spreads TooltipProps to the Tooltip component', async () => { + const user = userEvent.setup(); + createWrapper({ TooltipProps: { placement: 'left-start' } }); + + await act(async () => { + await user.hover(screen.getByRole('button')); + }); + expect(await screen.findByRole('tooltip')).toHaveTextContent('The Label'); - expect(wrapper.find(Tooltip).props().style).toEqual({ color: 'red' }); + expect(screen.getByText('The Label')).toHaveClass('MuiTooltip-tooltipPlacementLeft'); }); it('spreads any other props to IconButton', () => { - wrapper = createWrapper({ color: 'inherit' }); + createWrapper({ color: 'inherit' }); - expect(wrapper.find(Tooltip).find(IconButton).props().color).toEqual('inherit'); + expect(screen.getByRole('button')).toHaveClass('MuiIconButton-colorInherit'); }); it('wraps the child component in a badge if the badge prop is set to true (and passes BadgeProps)', () => { - wrapper = createWrapper({ badge: true, BadgeProps: { badgeContent: 3 } }); + createWrapper({ badge: true, BadgeProps: { badgeContent: 3 } }); - expect(wrapper.find(Badge).props().badgeContent).toEqual(3); - expect(wrapper.find(Badge).first().children().text()).toEqual('icon'); + expect(screen.getByRole('button')).toHaveTextContent('icon3'); + expect(screen.getByText('3')).toHaveClass('MuiBadge-badge'); }); }); diff --git a/__tests__/src/components/MosaicRenderPreview.test.js b/__tests__/src/components/MosaicRenderPreview.test.js index 929e492fc2d8feee408113e29f77cbe0d68fc9dc..9739efe160b4231a86f74aec6bce6ec0ae258e7f 100644 --- a/__tests__/src/components/MosaicRenderPreview.test.js +++ b/__tests__/src/components/MosaicRenderPreview.test.js @@ -1,10 +1,10 @@ -import { shallow } from 'enzyme'; -import MinimalWindow from '../../../src/containers/MinimalWindow'; +import { render, screen } from 'test-utils'; + import { MosaicRenderPreview } from '../../../src/components/MosaicRenderPreview'; describe('MosaicRenderPreview', () => { it('it renders the given title prop passed through the t prop function', () => { - const wrapper = shallow( + render( <MosaicRenderPreview t={(k, args) => `${k} ${args.title}`} title="The Title Prop" @@ -12,10 +12,6 @@ describe('MosaicRenderPreview', () => { />, ); - expect(wrapper.find(MinimalWindow).length).toBe(1); - expect( - wrapper.find(MinimalWindow).prop('label'), - ).toEqual('previewWindowTitle The Title Prop'); - expect(wrapper.find(MinimalWindow).prop('ariaLabel')).toEqual(false); + expect(screen.getByRole('heading')).toHaveTextContent('previewWindowTitle The Title Prop'); }); }); diff --git a/__tests__/src/components/NestedMenu.test.js b/__tests__/src/components/NestedMenu.test.js index e0849fe8cf4b80588500bbee93e7336ba31000cd..10af530a0666fbdfc8fa3fec677eff0f2d136f72 100644 --- a/__tests__/src/components/NestedMenu.test.js +++ b/__tests__/src/components/NestedMenu.test.js @@ -1,16 +1,12 @@ -import { shallow } from 'enzyme'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import ListItemText from '@material-ui/core/ListItemText'; -import ExpandLessIcon from '@material-ui/icons/ExpandLessSharp'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMoreSharp'; -import MenuItem from '@material-ui/core/MenuItem'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { NestedMenu } from '../../../src/components/NestedMenu'; /** * Helper function to wrap creating a NestedMenu component */ function createWrapper(props) { - return shallow( + return render( <NestedMenu icon="GivenIcon" label="GivenLabel" @@ -22,62 +18,40 @@ function createWrapper(props) { } describe('NestedMenu', () => { - let wrapper; - it('renders the given icon wrapped in a MUI ListItemIcon', () => { - wrapper = createWrapper(); + createWrapper(); - expect(wrapper.find(ListItemIcon).children().text()).toEqual('GivenIcon'); + expect(screen.getByText('GivenIcon')).toHaveClass('MuiListItemIcon-root'); }); it('does not render a ListItemIcon if no icon prop is passed', () => { - wrapper = createWrapper({ icon: null }); + createWrapper({ icon: null }); - expect(wrapper.find(ListItemIcon).length).toBe(0); + expect(screen.queryByText('GivenIcon')).not.toBeInTheDocument(); }); it('renders the given label wrapped in a MUI Typography', () => { - wrapper = createWrapper(); - - expect(wrapper.find(ListItemText).children().text()).toEqual('GivenLabel'); - }); - - it('toggles the local nestedMenuIsOpen state when clicking the MenuItem', () => { - wrapper = createWrapper(); + createWrapper(); - expect(wrapper.state().nestedMenuIsOpen).toBe(false); - wrapper.find(MenuItem).simulate('click'); - expect(wrapper.state().nestedMenuIsOpen).toBe(true); - wrapper.find(MenuItem).simulate('click'); - expect(wrapper.state().nestedMenuIsOpen).toBe(false); + expect(screen.getByText('GivenLabel')).toHaveClass('MuiTypography-body1'); }); - it('spreads options to the MenuItem', () => { - wrapper = createWrapper({ divider: true }); + it('toggles the local open state when clicking the MenuItem', async () => { + const user = userEvent.setup(); + createWrapper(); - expect(wrapper.find(MenuItem).props().divider).toBe(true); - }); + expect(screen.queryByText('GivenChildren')).not.toBeInTheDocument(); - it('renders the appropriate expand/collapse icon based on the menu open state', () => { - wrapper = createWrapper(); + await user.click(screen.getByRole('menuitem')); + expect(screen.getByText('GivenChildren')).toBeInTheDocument(); - expect(wrapper.state().nestedMenuIsOpen).toBe(false); - expect(wrapper.find(ExpandMoreIcon).length).toBe(1); - expect(wrapper.find(ExpandLessIcon).length).toBe(0); - wrapper.setState({ nestedMenuIsOpen: true }); - expect(wrapper.find(ExpandMoreIcon).length).toBe(0); - expect(wrapper.find(ExpandLessIcon).length).toBe(1); + await user.click(screen.getByRole('menuitem')); + expect(screen.queryByText('GivenChildren')).not.toBeInTheDocument(); }); - it("renders the component's children based on the nestedMenuIsOpen state", () => { - wrapper = createWrapper(); - - expect(wrapper.state().nestedMenuIsOpen).toBe(false); - expect(wrapper.children().length).toBe(1); - expect(wrapper.children().text()).not.toMatch(/GivenChildren/); + it('spreads options to the MenuItem', () => { + createWrapper({ 'data-testid': 'subject' }); - wrapper.setState({ nestedMenuIsOpen: true }); - expect(wrapper.children().length).toBe(2); - expect(wrapper.children().last().text()).toEqual('GivenChildren'); + expect(screen.getByTestId('subject')).toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/NewWindow.test.js b/__tests__/src/components/NewWindow.test.js index 1d09dd50268da603efa4e1a38881047924a98e39..160190078331eeeba2fbb79507be9091cd4a50f8 100644 --- a/__tests__/src/components/NewWindow.test.js +++ b/__tests__/src/components/NewWindow.test.js @@ -1,4 +1,4 @@ -import { shallow } from 'enzyme'; +import { render } from 'test-utils'; import { NewWindow } from '../../../src/components/NewWindow'; jest.useFakeTimers(); @@ -7,7 +7,7 @@ jest.useFakeTimers(); * Helper function to create a shallow wrapper around ErrorDialog */ function createWrapper(props) { - return shallow( + return render( <NewWindow url="http://example.com/" onClose={() => {}} @@ -18,7 +18,7 @@ function createWrapper(props) { describe('NewWindow', () => { it('renders properly and runs callbacks when the window closes', () => { - const mockWindow = {}; + const mockWindow = { close: jest.fn() }; const open = jest.fn(() => mockWindow); const onClose = jest.fn(); diff --git a/__tests__/src/components/OpenSeadragonViewer.test.js b/__tests__/src/components/OpenSeadragonViewer.test.js index 39dc6cbd9edc23abe68ed344b6b4811d7e84d119..480f41c16d76d9259a62861be1841f0bae8e0c57 100644 --- a/__tests__/src/components/OpenSeadragonViewer.test.js +++ b/__tests__/src/components/OpenSeadragonViewer.test.js @@ -1,4 +1,7 @@ -import { shallow } from 'enzyme'; +import { cloneElement } from 'react'; +import { render, screen, waitFor } from 'test-utils'; +import PropTypes from 'prop-types'; +import userEvent from '@testing-library/user-event'; import OpenSeadragon from 'openseadragon'; import { Utils } from 'manifesto.js'; import { OpenSeadragonViewer } from '../../../src/components/OpenSeadragonViewer'; @@ -7,13 +10,18 @@ import fixture from '../../fixtures/version-2/019.json'; const canvases = Utils.parseManifest(fixture).getSequences()[0].getCanvases(); -jest.mock('openseadragon'); - /** * Helper function to create a shallow wrapper around OpenSeadragonViewer */ function createWrapper(props) { - return shallow( + /** Stub child component for testing child props passing */ + const Child = ({ testId, zoomToWorld }) => <button type="button" data-testid={testId} onClick={zoomToWorld}>Child</button>; + Child.propTypes = { + testId: PropTypes.string.isRequired, + zoomToWorld: PropTypes.func.isRequired, + }; + + const component = ( <OpenSeadragonViewer classes={{}} infoResponses={[{ @@ -32,7 +40,7 @@ function createWrapper(props) { }, }]} nonTiledImages={[{ - getProperty: () => {}, + getProperty: () => { }, id: 'http://foo', }]} windowId="base" @@ -42,54 +50,75 @@ function createWrapper(props) { canvasWorld={new CanvasWorld(canvases)} {...props} > - <div className="foo" /> - <div className="bar" /> - </OpenSeadragonViewer>, + <Child testId="foo" /> + </OpenSeadragonViewer> ); + + const rendered = render(component); + + const viewer = OpenSeadragon.getViewer(screen.getByLabelText('item')); + + return { ...rendered, component, viewer }; } describe('OpenSeadragonViewer', () => { - let wrapper; - let updateViewport; + let user; beforeEach(() => { - OpenSeadragon.mockClear(); - wrapper = createWrapper({}); - updateViewport = wrapper.instance().props.updateViewport; + user = userEvent.setup(); }); it('renders the component', () => { - expect(wrapper.find('.mirador-osd-container').length).toBe(1); + createWrapper({}); + expect(screen.getByLabelText('item')).toHaveClass('mirador-osd-container'); }); - it('renders child components enhanced with additional props', () => { - expect(wrapper.find('.foo').length).toBe(1); - expect(wrapper.find('.foo').props()).toEqual(expect.objectContaining({ - zoomToWorld: wrapper.instance().zoomToWorld, - })); - expect(wrapper.find('.bar').length).toBe(1); - expect(wrapper.find('.bar').props()).toEqual(expect.objectContaining({ - zoomToWorld: wrapper.instance().zoomToWorld, - })); + it('renders child components enhanced with additional props', async () => { + const { viewer } = createWrapper({}); + const fitBounds = jest.fn(); + jest.replaceProperty(viewer, 'viewport', { fitBounds }); + + await user.click(screen.getByTestId('foo')); + expect(fitBounds).toHaveBeenCalled(); }); describe('infoResponsesMatch', () => { it('when they do not match', () => { - expect(wrapper.instance().infoResponsesMatch([])).toBe(false); + const { component, rerender, viewer } = createWrapper({}); + const mockClose = jest.spyOn(viewer, 'close'); + + rerender(cloneElement(component, { infoResponses: [] })); + + expect(mockClose).toHaveBeenCalled(); }); it('with an empty array', () => { - wrapper = createWrapper({ infoResponses: [] }); - expect(wrapper.instance().infoResponsesMatch([])).toBe(true); + const { component, rerender, viewer } = createWrapper({ infoResponses: [] }); + const mockClose = jest.spyOn(viewer, 'close'); + + rerender(cloneElement(component, { infoResponses: [] })); + + expect(mockClose).not.toHaveBeenCalled(); }); it('when the @ids do match', () => { + const { component, rerender, viewer } = createWrapper({}); + const mockClose = jest.spyOn(viewer, 'close'); + const newInfos = [ { id: 'a', json: { '@id': 'http://foo' } }, { id: 'b', json: { '@id': 'http://bar' } }, ]; - expect(wrapper.instance().infoResponsesMatch(newInfos)).toBe(true); + + rerender(cloneElement(component, { infoResponses: newInfos })); + + expect(mockClose).not.toHaveBeenCalled(); }); it('when the @ids do not match', () => { - expect(wrapper.instance().infoResponsesMatch([{ id: 'a', json: { '@id': 'http://foo-degraded' } }])).toBe(false); + const { component, rerender, viewer } = createWrapper({}); + const mockClose = jest.spyOn(viewer, 'close'); + + rerender(cloneElement(component, { infoResponses: [{ id: 'a', json: { '@id': 'http://foo-degraded' } }] })); + + expect(mockClose).toHaveBeenCalled(); }); it('when the id props match', () => { - wrapper = createWrapper({ + const { component, rerender, viewer } = createWrapper({ infoResponses: [{ id: 'a', json: { @@ -99,76 +128,86 @@ describe('OpenSeadragonViewer', () => { }, }], }); - expect(wrapper.instance().infoResponsesMatch([{ id: 'a', json: { id: 'http://foo' } }])).toBe(true); + const mockClose = jest.spyOn(viewer, 'close'); + rerender(cloneElement(component, { infoResponses: [{ id: 'a', json: { id: 'http://foo' } }] })); + expect(mockClose).not.toHaveBeenCalled(); }); }); describe('nonTiledImagedMatch', () => { it('when they do not match', () => { - expect(wrapper.instance().nonTiledImagedMatch([])).toBe(false); + const { component, rerender, viewer } = createWrapper({}); + const mockClose = jest.spyOn(viewer, 'close'); + + rerender(cloneElement(component, { nonTiledImages: [] })); + expect(mockClose).toHaveBeenCalled(); }); it('with an empty array', () => { - wrapper = createWrapper({ nonTiledImages: [] }); - expect(wrapper.instance().nonTiledImagedMatch([])).toBe(true); + const { component, rerender, viewer } = createWrapper({ nonTiledImages: [] }); + const mockClose = jest.spyOn(viewer, 'close'); + + rerender(cloneElement(component, { nonTiledImages: [] })); + expect(mockClose).not.toHaveBeenCalled(); }); it('when the ids do match', () => { - expect(wrapper.instance().nonTiledImagedMatch([{ id: 'http://foo' }])).toBe(true); + const { component, rerender, viewer } = createWrapper({}); + const mockClose = jest.spyOn(viewer, 'close'); + + rerender(cloneElement(component, { nonTiledImages: [{ id: 'http://foo' }] })); + expect(mockClose).not.toHaveBeenCalled(); }); }); describe('addAllImageSources', () => { it('calls addTileSource for every tileSources and then zoomsToWorld', async () => { - wrapper = createWrapper({ infoResponses: [1, 2, 3, 4] }); - wrapper.setState({ viewer: { viewport: { fitBounds: () => {} }, world: { getItemCount: () => 0 } } }); - const mockAddTileSource = jest.fn(); - wrapper.instance().addTileSource = mockAddTileSource; - await wrapper.instance().addAllImageSources(); - expect(mockAddTileSource).toHaveBeenCalledTimes(4); + const { component, rerender, viewer } = createWrapper({ infoResponses: [] }); + + const mockAddTiledImage = jest.spyOn(viewer, 'addTiledImage'); + const mockFitBounds = jest.spyOn(viewer.viewport, 'fitBounds'); + + rerender(cloneElement(component, { infoResponses: [{ id: 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44' }, { id: 'https://stacks.stanford.edu/image/iiif/fr426cg9537%2FSC1094_s3_b14_f17_Cats_1976_0005' }] })); + + expect(mockAddTiledImage).toHaveBeenCalledTimes(2); + await waitFor(() => expect(mockFitBounds).toHaveBeenCalled()); }); it('calls addNonTileSource for every nonTiledImage and then zoomsToWorld', async () => { - wrapper = createWrapper({ + const { component, rerender, viewer } = createWrapper({ nonTiledImages: [] }); + + const mockAddNonTiledImage = jest.spyOn(viewer, 'addSimpleImage'); + const mockFitBounds = jest.spyOn(viewer.viewport, 'fitBounds'); + + rerender(cloneElement(component, { nonTiledImages: [ { getProperty: () => 'Image' }, { getProperty: () => 'Image' }, { getProperty: () => 'Image' }, { getProperty: () => 'Image' }, ], - }); - const instance = wrapper.instance(); - const mockAddNonTiledImage = jest.fn(); - wrapper.instance().addNonTiledImage = mockAddNonTiledImage; - await instance.addAllImageSources(); + })); + expect(mockAddNonTiledImage).toHaveBeenCalledTimes(4); - }); - }); - describe('addTileSource', () => { - it('when a viewer is not available, returns an unresolved Promise', () => ( - expect(wrapper.instance().addTileSource({})).rejects.toBeUndefined() - )); + await waitFor(() => expect(mockFitBounds).toHaveBeenCalled()); + }); }); describe('addNonTiledImage', () => { it('calls addSimpleImage asynchronously on the OSD viewer', () => { - const viewer = {}; - viewer.addSimpleImage = ({ success }) => { success('event'); }; - wrapper.instance().setState({ viewer }); - - return wrapper.instance() - .addNonTiledImage({ getProperty: () => 'Image' }) - .then((event) => { - expect(event).toBe('event'); - }); + const { component, rerender, viewer } = createWrapper({ nonTiledImages: [] }); + const mockAdd = jest.spyOn(viewer, 'addSimpleImage'); + rerender(cloneElement(component, { nonTiledImages: [{ getProperty: () => 'Image', id: 'a' }] })); + + expect(mockAdd).toHaveBeenCalledWith(expect.objectContaining({ url: 'a' })); }); - it('calls addSimpleImage asynchronously on the OSD viewer', () => ( - wrapper.instance() - .addNonTiledImage({ getProperty: () => 'Video' }) - .then((event) => { - expect(event).toBe(undefined); - }) - )); + it('only calls addSimpleImage for images', () => { + const { component, rerender, viewer } = createWrapper({ nonTiledImages: [] }); + const mockAdd = jest.spyOn(viewer, 'addSimpleImage'); + rerender(cloneElement(component, { nonTiledImages: [{ getProperty: () => 'Video', id: 'a' }] })); + + expect(mockAdd).not.toHaveBeenCalled(); + }); }); describe('refreshTileProperties', () => { @@ -179,18 +218,21 @@ describe('OpenSeadragonViewer', () => { contentResource: i => i, layerIndexOfImageResource: i => 1 - i, layerOpacityOfImageResource: i => 0.5, + layers: [{ id: 'a' }, { id: 'b' }], + worldBounds: () => ([0, 0, 100, 100]), }; - wrapper = createWrapper({ canvasWorld }); - wrapper.instance().loaded = true; - wrapper.instance().state.viewer = { - world: { - getItemAt: i => ({ setOpacity, source: { id: i } }), - getItemCount: () => 2, - setItemIndex, - }, + const { component, rerender, viewer } = createWrapper({ canvasWorld }); + + const newCanvasWorld = { + ...canvasWorld, + layers: [{ id: 'a' }, { id: 'c' }], }; - wrapper.instance().refreshTileProperties(); + jest.spyOn(viewer.world, 'getItemAt').mockImplementation(i => ({ setOpacity, source: { id: i } })); + jest.spyOn(viewer.world, 'setItemIndex').mockImplementation(setItemIndex); + jest.spyOn(viewer.world, 'getItemCount').mockImplementation(() => 2); + + rerender(cloneElement(component, { canvasWorld: newCanvasWorld })); expect(setOpacity).toHaveBeenCalledTimes(1); expect(setOpacity.mock.calls[0]).toEqual([0.5]); @@ -201,152 +243,77 @@ describe('OpenSeadragonViewer', () => { }); }); - describe('fitBounds', () => { - it('calls OSD viewport.fitBounds with provided x, y, w, h', () => { - const fitBounds = jest.fn(); - - wrapper.setState({ - viewer: { - viewport: { - fitBounds, - }, - }, - }); - - wrapper.instance().fitBounds(1, 2, 3, 4); - expect( - fitBounds, - ).toHaveBeenCalledWith(expect.any(OpenSeadragon.Rect), true); - }); - }); - describe('zoomToWorld', () => { - it('uses fitBounds with the existing CanvasWorld', () => { + it('uses fitBounds with the existing CanvasWorld', async () => { + const { viewer } = createWrapper({}); const fitBounds = jest.fn(); - wrapper.instance().fitBounds = fitBounds; - wrapper.instance().zoomToWorld(); - expect(fitBounds).toHaveBeenCalledWith(0, 0, 5041, 1800, true); + jest.replaceProperty(viewer, 'viewport', { fitBounds }); + + await user.click(screen.getByTestId('foo')); + expect(fitBounds).toHaveBeenCalledWith({ + degrees: 0, height: 1800, width: 5041, x: 0, y: 0, + }, expect.anything()); }); }); describe('componentDidMount', () => { - let panTo; - let zoomTo; - let addHandler; - let innerTracker; + let wrapper; beforeEach(() => { - panTo = jest.fn(); - zoomTo = jest.fn(); - addHandler = jest.fn(); - innerTracker = {}; - - wrapper = shallow( - <OpenSeadragonViewer - classes={{}} - tileSources={[{ '@id': 'http://foo' }]} - windowId="base" - viewerConfig={{ x: 1, y: 0, zoom: 0.5 }} - config={{}} - updateViewport={updateViewport} - canvasWorld={new CanvasWorld([])} - t={k => k} - > - <div className="foo" /> - </OpenSeadragonViewer>, - ); - - wrapper.instance().ref = { current: true }; - - OpenSeadragon.mockImplementation(() => ({ - addHandler, - addTiledImage: jest.fn().mockResolvedValue('event'), - innerTracker, - viewport: { panTo, zoomTo }, - })); - }); - - it('calls the OSD viewport panTo and zoomTo with the component state', () => { - wrapper.instance().componentDidMount(); - - expect(panTo).toHaveBeenCalledWith({ x: 1, y: 0, zoom: 0.5 }, true); - expect(zoomTo).toHaveBeenCalledWith(0.5, { x: 1, y: 0, zoom: 0.5 }, true); - }); - - it('adds animation-start/finish flag for rerendering performance', () => { - wrapper.instance().componentDidMount(); - - expect(addHandler).toHaveBeenCalledWith('animation-start', expect.anything()); - expect(addHandler).toHaveBeenCalledWith('animation-finish', expect.anything()); - expect(addHandler).toHaveBeenCalledWith('animation-finish', wrapper.instance().onViewportChange); + wrapper = createWrapper({ + canvasWorld: new CanvasWorld([]), + tileSources: [{ '@id': 'http://foo' }], + viewerConfig: { x: 1, y: 0, zoom: 0.5 }, + }); }); - it('adds a mouse-move handler', () => { - wrapper.instance().componentDidMount(); - - expect(innerTracker.moveHandler).toEqual(wrapper.instance().onCanvasMouseMove); + it('controls the OSD viewport pan and zoom', () => { + expect(wrapper.viewer.viewport.getZoom()).toBe(0.5); + expect(wrapper.viewer.viewport.getCenter()).toEqual({ x: 1, y: 0 }); }); }); describe('componentDidUpdate', () => { it('calls the OSD viewport panTo and zoomTo with the component state and forces a redraw', () => { - const panTo = jest.fn(); - const zoomTo = jest.fn(); - const setFlip = jest.fn(); - const setRotation = jest.fn(); - const forceRedraw = jest.fn(); - - wrapper.setState({ - viewer: { - forceRedraw, - viewport: { - centerSpringX: { target: { value: 10 } }, - centerSpringY: { target: { value: 10 } }, - getFlip: () => false, - getRotation: () => (0), - panTo, - setFlip, - setRotation, - zoomSpring: { target: { value: 1 } }, - zoomTo, - }, - }, - }); + const { component, rerender, viewer } = createWrapper({}); - wrapper.setProps({ + rerender(cloneElement(component, { viewerConfig: { flip: false, rotation: 90, x: 0.5, y: 0.5, zoom: 0.1, }, - }); + })); + expect(viewer.viewport.getFlip()).toBe(false); + expect(viewer.viewport.getRotation()).toBe(90); + expect(viewer.viewport.getZoom()).toBe(0.1); + expect(viewer.viewport.getCenter()).toEqual({ x: 0.5, y: 0.5 }); - wrapper.setProps({ + rerender(cloneElement(component, { viewerConfig: { flip: true, rotation: 0, x: 1, y: 0, zoom: 0.5, }, - }); - - expect(panTo).toHaveBeenCalledWith(expect.objectContaining({ x: 1, y: 0, zoom: 0.5 }), false); - expect(zoomTo).toHaveBeenCalledWith(0.5, expect.objectContaining({ x: 1, y: 0, zoom: 0.5 }), false); - expect(setRotation).toHaveBeenCalledWith(90); - expect(setFlip).toHaveBeenCalledWith(true); - expect(forceRedraw).not.toHaveBeenCalled(); + })); + expect(viewer.viewport.getFlip()).toBe(true); + expect(viewer.viewport.getRotation()).toBe(0); + expect(viewer.viewport.getZoom()).toBe(0.5); + expect(viewer.viewport.getCenter()).toEqual({ x: 1, y: 0 }); }); }); describe('onViewportChange', () => { it('translates the OSD viewport data into an update to the component state', () => { - wrapper.instance().onViewportChange({ - eventSource: { - viewport: { - centerSpringX: { target: { value: 1 } }, - centerSpringY: { target: { value: 0 } }, - getFlip: () => false, - getRotation: () => 90, - zoomSpring: { target: { value: 0.5 } }, - }, - }, + const updateViewport = jest.fn(); + const { viewer } = createWrapper({ updateViewport }); + + jest.replaceProperty(viewer, 'viewport', { + centerSpringX: { target: { value: 1 } }, + centerSpringY: { target: { value: 0 } }, + getFlip: () => false, + getRotation: () => 90, + zoomSpring: { target: { value: 0.5 } }, }); + viewer.raiseEvent('animation-finish'); + expect(updateViewport).toHaveBeenCalledWith( 'base', { @@ -358,13 +325,19 @@ describe('OpenSeadragonViewer', () => { describe('onCanvasMouseMove', () => { it('triggers an OSD event', () => { - const viewer = { raiseEvent: jest.fn() }; - wrapper.setState({ viewer }); + jest.useFakeTimers(); + const { viewer } = createWrapper({}); + const mockHandler = jest.fn(); + viewer.addHandler('mouse-move', mockHandler); + + const e = new MouseEvent('mousemove', { clientX: 0, clientY: 0 }); + viewer.innerTracker.moveHandler(e); + + jest.advanceTimersByTime(100); - wrapper.instance().onCanvasMouseMove('event'); - wrapper.instance().onCanvasMouseMove.flush(); + expect(mockHandler).toHaveBeenCalledWith(e); - expect(viewer.raiseEvent).toHaveBeenCalledWith('mouse-move', 'event'); + jest.useRealTimers(); }); }); }); diff --git a/__tests__/src/components/PluginHook.test.js b/__tests__/src/components/PluginHook.test.js index bda3c70e057c630525b07098559b7ec0ced23363..eae8dbd6f93cb4f21beb4f7b22bdca08cd7a7468 100644 --- a/__tests__/src/components/PluginHook.test.js +++ b/__tests__/src/components/PluginHook.test.js @@ -1,34 +1,41 @@ -import { shallow } from 'enzyme'; +import { render, screen } from 'test-utils'; import { PluginHook } from '../../../src/components/PluginHook'; -it('renders nothing when no plugins passed', () => { - const wrapper = shallow(<PluginHook />); - expect(wrapper).toEqual({}); -}); - -/** */ -const PluginComponentA = props => <div>A</div>; /** */ -const PluginComponentB = props => <div>B</div>; +const mockComponentA = () => ( + <div data-testid="testA" /> +); -it('renders plugin components if some passed', () => { - const wrapper = shallow( - <PluginHook - PluginComponents={[PluginComponentA, PluginComponentB]} - />, - ); +/** */ +const mockComponentB = () => ( + <div data-testid="testB" /> +); - expect(wrapper.find(PluginComponentA).length).toBe(1); - expect(wrapper.find(PluginComponentB).length).toBe(1); -}); +describe('WindowTopBarPluginArea', () => { + it('renders nothing when no plugins passed', () => { + render(<PluginHook />); + expect(screen.queryByTestId('testA')).not.toBeInTheDocument(); + expect(screen.queryByTestId('testB')).not.toBeInTheDocument(); + }); -it('does not pass classes to PluginComponents (which will throw warnings for styles plugins)', () => { - const wrapper = shallow( - <PluginHook - classes={{ someLocal: 'classes' }} - PluginComponents={[PluginComponentA]} - />, - ); + it('renders plugin components if some passed', () => { + render( + <PluginHook + PluginComponents={[mockComponentA, mockComponentB]} + />, + ); + expect(screen.getByTestId('testA')).toBeInTheDocument(); + expect(screen.getByTestId('testB')).toBeInTheDocument(); + }); - expect(wrapper.find(PluginComponentA).props().classes).toBeUndefined(); + it('does not pass classes to PluginComponents (which will throw warnings for styles plugins)', () => { + render( + <PluginHook + classes={{ someLocal: 'classes' }} + PluginComponents={[mockComponentA]} + />, + ); + // if called with nothing passed as args, .toHaveClass checks for existence of any classes + expect(screen.getByTestId('testA')).not.toHaveClass(); + }); }); diff --git a/__tests__/src/components/PrimaryWindow.test.js b/__tests__/src/components/PrimaryWindow.test.js index 7f706545f0658fb439bdcbb6a939cad57388ffe6..ad78ebf4b5cf39c5f7433721471429ded5b96237 100644 --- a/__tests__/src/components/PrimaryWindow.test.js +++ b/__tests__/src/components/PrimaryWindow.test.js @@ -1,56 +1,49 @@ -import { shallow } from 'enzyme'; +import { render, screen } from 'test-utils'; import { PrimaryWindow } from '../../../src/components/PrimaryWindow'; -import WindowSideBar from '../../../src/containers/WindowSideBar'; -import CollectionDialog from '../../../src/containers/CollectionDialog'; /** create wrapper */ function createWrapper(props) { - return shallow( + return render( <PrimaryWindow classes={{}} - windowId="window-1" + windowId="xyz" {...props} />, + { preloadedState: { windows: { xyz: { collectionPath: [{}], companionWindowIds: [] } } } }, ); } describe('PrimaryWindow', () => { - it('should render outer element', () => { - const wrapper = createWrapper(); - expect(wrapper.find('.mirador-primary-window')).toHaveLength(1); + it('should render expected elements', async () => { + createWrapper({ isFetching: false }); + await screen.findByTestId('test-window'); + expect(document.querySelector('.mirador-primary-window')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access + expect(document.querySelector('.mirador-companion-area-left')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access }); - it('should only render children when available', () => { - const wrapper = createWrapper({ children: <span>hi</span>, isFetching: false }); - expect(wrapper.find('span')).toHaveLength(1); - const suspenseComponent = wrapper.find('Suspense'); - const lazyComponent = suspenseComponent.dive().find('lazy'); - expect(lazyComponent).toHaveLength(0); + it('should render children when available', () => { + createWrapper({ children: <span>hi</span>, isFetching: false }); + expect(screen.getByText('hi')).toBeInTheDocument(); }); - it('should render <WindowSideBar>', () => { - const wrapper = createWrapper(); - expect(wrapper.find(WindowSideBar)).toHaveLength(1); + it('should render nothing if still fetching', () => { + createWrapper({ isFetching: true }); + expect(screen.queryByRole('region', { accessibleName: 'item' })).not.toBeInTheDocument(); }); - it('should render nothing if manifest is still fetching', () => { - const wrapper = createWrapper({ isFetching: true }); - const suspenseComponent = wrapper.find('Suspense'); - expect(suspenseComponent).toEqual({}); + it('should render <GalleryView> if fetching is complete and view is gallery', async () => { + createWrapper({ isFetching: false, view: 'gallery' }); + await screen.findByTestId('test-window'); + expect(document.querySelector('#xyz-gallery')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access }); - it('should render <WindowViewer> if manifest is present', () => { - const wrapper = createWrapper({ isFetching: false }); - const suspenseComponent = wrapper.find('Suspense'); - const lazyComponent = suspenseComponent.dive().find('lazy'); - expect(lazyComponent.type().displayName).toBe('WindowViewer'); - }); - it('should render <GalleryView> if manifest is present and view is gallery', () => { - const wrapper = createWrapper({ isFetching: false, view: 'gallery', windowId: 'window-2' }); - const suspenseComponent = wrapper.find('Suspense'); - const lazyComponent = suspenseComponent.dive().find('lazy'); - expect(lazyComponent.type().displayName).toBe('GalleryView'); - }); - it('should render <CollectionDialog> and <SelectCollection> if manifest is collection and isCollectionDialogVisible', () => { - const wrapper = createWrapper({ isCollection: true, isCollectionDialogVisible: true }); - const lazyComponent = wrapper.find('lazy'); - expect(lazyComponent.type().displayName).toBe('SelectCollection'); - expect(wrapper.find(CollectionDialog)).toHaveLength(1); + it('should render <CollectionDialog> and <SelectCollection> if manifest is collection and isCollectionDialogVisible', async () => { + render(<div id="xyz" />); + render( + <PrimaryWindow + classes={{}} + isCollection + isCollectionDialogVisible + windowId="xyz" + />, + { preloadedState: { windows: { xyz: { collectionPath: [{}] } } } }, + ); + await screen.findByRole('button', { accessibleName: 'show collection' }); }); }); diff --git a/__tests__/src/components/SanitizedHtml.test.js b/__tests__/src/components/SanitizedHtml.test.js index c3bcf3e7429698c90b2e4a6dfce0a18f46551778..382b90570e5ca8748fab1db2d7da25746e9c1f90 100644 --- a/__tests__/src/components/SanitizedHtml.test.js +++ b/__tests__/src/components/SanitizedHtml.test.js @@ -1,25 +1,29 @@ -import { shallow } from 'enzyme'; +import { render, screen } from 'test-utils'; import { SanitizedHtml } from '../../../src/components/SanitizedHtml'; -const wrapper = shallow( - <SanitizedHtml - classes={{ root: 'root' }} - htmlString="<script>doBadThings()</script><b>Don't worry!</b><a>Some link</a>" - ruleSet="iiif" - />, -); - describe('SanitizedHtml', () => { + beforeEach(() => { + render( + <SanitizedHtml + data-testid="subject" + htmlString="<script>doBadThings()</script><b>Don't worry!</b><a>Some link</a>" + ruleSet="iiif" + />, + ); + }); + it('should render needed elements', () => { - expect(wrapper.find('span').length).toBe(1); + expect(screen.getByTestId('subject')).toHaveProperty('tagName', 'SPAN'); }); it('should pass correct class name to root element', () => { - expect(wrapper.find('span').first().props().className).toBe('root mirador-third-party-html'); + expect(screen.getByTestId('subject')).toHaveClass('mirador-third-party-html'); }); it('should pass sanitized html string to dangerouslySetInnerHTML attribute', () => { - expect(wrapper.find('span').first().props().dangerouslySetInnerHTML) - .toEqual({ __html: "<b>Don't worry!</b><a target=\"_blank\" rel=\"noopener noreferrer\">Some link</a>" }); + expect(screen.getByTestId('subject').querySelector('script')).not.toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access, testing-library/prefer-presence-queries + expect(screen.getByText('Don\'t worry!')).toBeInTheDocument(); + expect(screen.getByText('Some link')).toHaveAttribute('target', '_blank'); + expect(screen.getByText('Some link')).toHaveAttribute('rel', 'noopener noreferrer'); }); }); diff --git a/__tests__/src/components/ScrollIndicatedDialogContent.test.js b/__tests__/src/components/ScrollIndicatedDialogContent.test.js index 0f39735c9304055d887d137fdec2780e66a7d807..ae84979485a464c2d6b98928a42082833702c223 100644 --- a/__tests__/src/components/ScrollIndicatedDialogContent.test.js +++ b/__tests__/src/components/ScrollIndicatedDialogContent.test.js @@ -1,36 +1,25 @@ -import { shallow } from 'enzyme'; -import DialogContent from '@material-ui/core/DialogContent'; +import { render, screen } from 'test-utils'; import { ScrollIndicatedDialogContent } from '../../../src/components/ScrollIndicatedDialogContent'; /** Utility function to wrap */ function createWrapper(props) { - return shallow( + return render( <ScrollIndicatedDialogContent - classes={{ shadowScrollDialog: 'shadowScrollDialog' }} + data-testid="subject" {...props} />, ); } describe('ScrollIndicatedDialogContent', () => { - let wrapper; - it('renders a DialogContnet component passing props', () => { - wrapper = createWrapper({ randomProp: 'randomPropValue' }); + createWrapper({ randomprop: 'randomPropValue' }); - expect(wrapper.find(DialogContent).length).toBe(1); - expect(wrapper.find(DialogContent).props().randomProp).toEqual('randomPropValue'); - }); - - it('provides a className to the DialogContent prop to style it', () => { - wrapper = createWrapper(); - - expect(wrapper.find(DialogContent).props().className).toMatch('shadowScrollDialog'); + expect(screen.getByTestId('subject')).toHaveAttribute('randomprop', 'randomPropValue'); }); it('joins an incoming className prop with our className', () => { - wrapper = createWrapper({ className: 'upstreamClassName' }); - - expect(wrapper.find(DialogContent).props().className).toMatch('upstreamClassName shadowScrollDialog'); + createWrapper({ className: 'upstreamClassName' }); + expect(screen.getByTestId('subject')).toHaveClass('upstreamClassName'); }); }); diff --git a/__tests__/src/components/ScrollTo.test.js b/__tests__/src/components/ScrollTo.test.js index c412e525fcca7f40101519e26aa3497985ab7bac..fd4f34f087645e865eb56725a97705c40b80f7ca 100644 --- a/__tests__/src/components/ScrollTo.test.js +++ b/__tests__/src/components/ScrollTo.test.js @@ -1,97 +1,82 @@ -import { shallow } from 'enzyme'; +import { render } from 'test-utils'; +import { createRef } from 'react'; import { ScrollTo } from '../../../src/components/ScrollTo'; -/** Utility function to wrap ScrollTo */ -function createWrapper(props) { - return shallow( - <ScrollTo - containerRef={() => {}} - scrollTo - {...props} - > - Child Prop - </ScrollTo>, - ); -} - describe('ScrollTo', () => { - let wrapper; - const scrollToElAboveBoundingRect = { bottom: -200, top: -300 }; - const scrollToElBelowBoundingRect = { bottom: 601, top: 501 }; - const visibleScrollToElBoundingRect = { bottom: 300, top: 200 }; + let containerRef; + const containerBoundingRect = { bottom: 500, height: 440, top: 0 }; + let scrollTo; + beforeEach(() => { + scrollTo = jest.fn(); + containerRef = createRef(); + render(<div data-testid="container" ref={containerRef} />); - it('wraps the given children in a div element', () => { - wrapper = createWrapper(); - expect(wrapper.find('div').length).toBe(1); - expect(wrapper.find('div').children().text()).toEqual('Child Prop'); + containerRef.current = { + getBoundingClientRect: () => containerBoundingRect, + getElementsByClassName: () => [{ scrollTo }], + }; }); + const scrollToElAboveBoundingRect = { bottom: -200, top: -300 }; + const scrollToElBelowBoundingRect = { bottom: 601, top: 501 }; + const visibleScrollToElBoundingRect = { bottom: 300, top: 200 }; + describe('when updating the scrollTo prop', () => { + beforeEach(() => { + jest.spyOn(ScrollTo.prototype, 'elementToScrollTo').mockImplementation(() => ({ offsetTop: 450 })); + }); describe('when setting from true to false', () => { it('does not scroll to the selected element', () => { - const scrollTo = jest.fn(); - jest.spyOn(ScrollTo.prototype, 'elementToScrollTo').mockImplementation(() => ({ offsetTop: 450 })); - jest.spyOn(ScrollTo.prototype, 'scrollabelContainer').mockImplementation(() => ({ scrollTo })); - jest.spyOn(ScrollTo.prototype, 'containerBoundingRect').mockImplementation(() => ({ - ...containerBoundingRect, - })); jest.spyOn(ScrollTo.prototype, 'scrollToBoundingRect').mockImplementation(() => ({ ...scrollToElAboveBoundingRect, })); - wrapper = createWrapper(); - wrapper.setProps({ scrollTo: false }); + + const { rerender } = render(<ScrollTo scrollTo containerRef={containerRef}><div>Child</div></ScrollTo>); // It is called once when initially rendered w/ true - expect(scrollTo).not.toHaveBeenCalledTimes(2); + expect(scrollTo).toHaveBeenCalled(); + scrollTo.mockReset(); + + rerender(<ScrollTo containerRef={containerRef}><div>Child</div></ScrollTo>); + + // But it is not called on the re-render w/ false + expect(scrollTo).not.toHaveBeenCalled(); }); }); describe('when set from false to true', () => { it('scrolls to the selected element when it is hidden above the container', () => { - const scrollTo = jest.fn(); - jest.spyOn(ScrollTo.prototype, 'elementToScrollTo').mockImplementation(() => ({ offsetTop: 450 })); - jest.spyOn(ScrollTo.prototype, 'scrollabelContainer').mockImplementation(() => ({ scrollTo })); - jest.spyOn(ScrollTo.prototype, 'containerBoundingRect').mockImplementation(() => ({ - ...containerBoundingRect, - })); jest.spyOn(ScrollTo.prototype, 'scrollToBoundingRect').mockImplementation(() => ({ ...scrollToElAboveBoundingRect, })); - wrapper = createWrapper({ scrollTo: false }); - wrapper.setProps({ scrollTo: true }); + const { rerender } = render(<ScrollTo containerRef={containerRef}><div>Child</div></ScrollTo>); + + rerender(<ScrollTo scrollTo containerRef={containerRef}><div>Child</div></ScrollTo>); expect(scrollTo).toHaveBeenCalledWith(0, 230); }); - it('scrolls to the selected element when it is hidden above the container', () => { - const scrollTo = jest.fn(); - jest.spyOn(ScrollTo.prototype, 'elementToScrollTo').mockImplementation(() => ({ offsetTop: 450 })); - jest.spyOn(ScrollTo.prototype, 'scrollabelContainer').mockImplementation(() => ({ scrollTo })); - jest.spyOn(ScrollTo.prototype, 'containerBoundingRect').mockImplementation(() => ({ - ...containerBoundingRect, - })); + it('scrolls to the selected element when it is hidden below the container', () => { jest.spyOn(ScrollTo.prototype, 'scrollToBoundingRect').mockImplementation(() => ({ ...scrollToElBelowBoundingRect, })); - wrapper = createWrapper({ scrollTo: false }); - wrapper.setProps({ scrollTo: true }); + + const { rerender } = render(<ScrollTo containerRef={containerRef}><div>Child</div></ScrollTo>); + + rerender(<ScrollTo scrollTo containerRef={containerRef}><div>Child</div></ScrollTo>); expect(scrollTo).toHaveBeenCalledWith(0, 230); }); it('does not scroll to the selected element when it is visible', () => { - const scrollTo = jest.fn(); - jest.spyOn(ScrollTo.prototype, 'elementToScrollTo').mockImplementation(() => ({ offsetTop: 450 })); - jest.spyOn(ScrollTo.prototype, 'scrollabelContainer').mockImplementation(() => ({ scrollTo })); - jest.spyOn(ScrollTo.prototype, 'containerBoundingRect').mockImplementation(() => ({ - ...containerBoundingRect, - })); jest.spyOn(ScrollTo.prototype, 'scrollToBoundingRect').mockImplementation(() => ({ ...visibleScrollToElBoundingRect, })); - wrapper = createWrapper({ scrollTo: false }); - wrapper.setProps({ scrollTo: true }); + + const { rerender } = render(<ScrollTo containerRef={containerRef}><div>Child</div></ScrollTo>); + + rerender(<ScrollTo scrollTo containerRef={containerRef}><div>Child</div></ScrollTo>); expect(scrollTo).not.toHaveBeenCalled(); }); diff --git a/__tests__/src/components/SearchHit.test.js b/__tests__/src/components/SearchHit.test.js index 9fd9fdbf7b051ad2080542bd739412fe22460811..e999a8b1e8efe2fa83a0294167d76ce2d21d49ae 100644 --- a/__tests__/src/components/SearchHit.test.js +++ b/__tests__/src/components/SearchHit.test.js @@ -1,110 +1,124 @@ -import { shallow } from 'enzyme'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; +import PropTypes from 'prop-types'; import { SearchHit } from '../../../src/components/SearchHit'; -import { ScrollTo } from '../../../src/components/ScrollTo'; + +/** Stub the ScrollTo so we can easily make sure it'll work */ +const ScrollToMock = ({ children }) => (<div data-testid="scrollto">{children}</div>); +ScrollToMock.propTypes = { + children: PropTypes.node.isRequired, +}; + +jest.mock( + '../../../src/components/ScrollTo', + () => ({ + ScrollTo: ScrollToMock, + }), +); /** * Helper function to create a shallow wrapper around SearchResults */ -function createWrapper(props) { - return shallow( - <SearchHit - announcer={() => {}} - annotation={{ targetId: 'x' }} - annotationId="foo" - classes={{ windowSelected: 'windowSelected' }} - hit={{ - after: ', and start the chainsaw', - annotations: ['foo'], - before: 'Light up the', - match: 'moose', - }} - windowId="window" - selected - index={0} - windowSelected - {...props} - />, - ); -} +const Subject = (props) => ( + <SearchHit + announcer={() => {}} + annotation={{ targetId: 'x' }} + annotationId="foo" + hit={{ + after: ', and start the chainsaw', + annotations: ['foo'], + before: 'Light up the', + match: 'moose', + }} + windowId="window" + selected + index={0} + windowSelected + {...props} + /> +); describe('SearchHit', () => { - it('renders a ListItem for each hit', () => { + it('renders a ListItem for each hit', async () => { + const user = userEvent.setup(); const selectAnnotation = jest.fn(); - const wrapper = createWrapper({ selectAnnotation }); - expect(wrapper.find('WithStyles(ForwardRef(ListItem))').length).toEqual(1); - expect(wrapper.find('WithStyles(ForwardRef(ListItem))').prop('selected')).toEqual(true); - expect(wrapper.find('WithStyles(ForwardRef(ListItem))').prop('className')).toEqual('windowSelected'); - expect(wrapper.find('WithStyles(ForwardRef(ListItemText))').render().text()).toEqual('1Light up the moose , and start the chai '); - expect(wrapper.find('strong').length).toEqual(1); - - wrapper.find('WithStyles(ForwardRef(ListItem))').simulate('click'); + render(<Subject selectAnnotation={selectAnnotation} />); + + expect(screen.getByRole('listitem')).toHaveClass('Mui-selected'); + expect(screen.getByRole('listitem')).toHaveTextContent('1Light up the moose , and start the chai more'); + + await user.click(screen.getByRole('button')); expect(selectAnnotation).toHaveBeenCalledWith('foo'); }); it('renders the annotation char if the hit is not available', () => { - const wrapper = createWrapper({ annotation: { chars: 'xyz' }, hit: undefined }); - expect(wrapper.find('WithStyles(ForwardRef(ListItemText))').render().text()).toEqual('1xyz'); + render(<Subject annotation={{ chars: 'xyz' }} hit={undefined} />); + + expect(screen.getByRole('listitem')).toHaveTextContent('1xyz'); }); it('renders a ScrollTo', () => { - const wrapper = createWrapper({ containerRef: 'ref' }); - expect(wrapper.find(ScrollTo).prop('containerRef')).toEqual('ref'); - expect(wrapper.find(ScrollTo).prop('scrollTo')).toEqual(true); + render(<Subject containerRef="ref" />); + + expect(screen.getByTestId('scrollto')).toBeInTheDocument(); }); describe('Annotation Labels', () => { it('renders the annotationLabel if present', () => { - const wrapper = createWrapper({ annotationLabel: 'The Anno Label' }); + render(<Subject annotationLabel="The Anno Label" />); - expect(wrapper.find('WithStyles(ForwardRef(Typography))[variant="subtitle2"]').length).toEqual(2); - expect(wrapper.find('WithStyles(ForwardRef(Typography))[variant="subtitle2"][children="The Anno Label"]').length).toEqual(1); + expect(screen.getByRole('heading', { level: 4, name: 'The Anno Label' })).toBeInTheDocument(); }); it('does not render the typography if no annotation label is present', () => { - const wrapper = createWrapper(); + render(<Subject />); - expect(wrapper.find('WithStyles(ForwardRef(Typography))[variant="subtitle2"]').length).toEqual(1); + expect(screen.getByRole('heading', { level: 4 })).toBeInTheDocument(); }); }); describe('announcer', () => { it('sends information about the annotation when selected', () => { const announcer = jest.fn(); - const wrapper = createWrapper({ + const props = { annotationLabel: 'The Annotation Label', announcer, canvasLabel: 'The Canvas Label', selected: false, total: 9, - }); + }; + const { rerender } = render(<Subject {...props} />); expect(announcer).not.toHaveBeenCalled(); - wrapper.setProps({ selected: true }); + + rerender(<Subject {...props} selected />); expect(announcer).toHaveBeenCalledWith( 'pagination The Canvas Label The Annotation Label Light up the moose , and start the chai', + 'polite', ); }); it('calls the announcer when initially rendered as selected', () => { const announcer = jest.fn(); - createWrapper({ announcer, selected: true }); + render(<Subject announcer={announcer} selected />); expect(announcer).toHaveBeenCalled(); }); it('does not call the announcer when initially rendered as unselected', () => { const announcer = jest.fn(); - createWrapper({ announcer, selected: false }); + render(<Subject announcer={announcer} selected={false} />); expect(announcer).not.toHaveBeenCalled(); }); it('does not send information about annotations that are not being deselected', () => { const announcer = jest.fn(); - const wrapper = createWrapper({ announcer, selected: true }); + const { rerender } = render(<Subject announcer={announcer} selected />); announcer.mockClear(); - wrapper.setProps({ selected: false }); + + rerender(<Subject announcer={announcer} selected={false} />); expect(announcer).not.toHaveBeenCalled(); }); }); diff --git a/__tests__/src/components/SearchPanel.test.js b/__tests__/src/components/SearchPanel.test.js index 2d8f7eeb1d97d7b6a5a719dc4591093f3e917f11..cd7b50ee01958a1ab2aff5109d3edd4e9b7ad274 100644 --- a/__tests__/src/components/SearchPanel.test.js +++ b/__tests__/src/components/SearchPanel.test.js @@ -1,14 +1,14 @@ -import { shallow } from 'enzyme'; -import Button from '@material-ui/core/Button'; -import CompanionWindow from '../../../src/containers/CompanionWindow'; -import SearchResults from '../../../src/containers/SearchResults'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; +import i18next from 'i18next'; + import { SearchPanel } from '../../../src/components/SearchPanel'; /** * Helper function to create a shallow wrapper around SearchPanel */ function createWrapper(props) { - return shallow( + return render( <SearchPanel id="xyz" fetchSearch={() => {}} @@ -16,57 +16,70 @@ function createWrapper(props) { windowId="window" {...props} />, + { preloadedState: { companionWindows: { xyz: { content: 'search' } } } }, ); } describe('SearchPanel', () => { it('renders a CompanionWindow', () => { - const wrapper = createWrapper(); - expect(wrapper.find(CompanionWindow).length).toEqual(1); + createWrapper(); + expect(screen.getByRole('complementary')).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'searchTitle' })).toBeInTheDocument(); }); it('passes a Clear chip as the CompanionWindow title prop', () => { - const wrapper = createWrapper({ query: 'Wolpertinger' }); + createWrapper({ query: 'Wolpertinger' }); - const title = wrapper.find(CompanionWindow).props().title.props.children; - expect(title[0]).toEqual('searchTitle'); - expect(title[1].type.displayName).toEqual('WithStyles(ForwardRef(Chip))'); + expect(screen.getByRole('heading', { name: /searchTitle/ })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'clearSearch' })).toBeInTheDocument(); }); - it('the Clear chip calls the removeSearch prop', () => { + it('the Clear chip calls the removeSearch prop', async () => { + const user = userEvent.setup(); const removeSearch = jest.fn(); - const wrapper = createWrapper({ query: 'Wolpertinger', removeSearch }); - const chip = wrapper.find(CompanionWindow).props().title.props.children[1]; - chip.props.onClick(); + createWrapper({ query: 'Wolpertinger', removeSearch }); + + await user.click(screen.getByRole('button', { name: 'clearSearch' })); + expect(removeSearch).toHaveBeenCalled(); }); it('does not render a Clear chip if there is no search query to be cleared', () => { - const wrapper = createWrapper(); + createWrapper(); - const title = wrapper.find(CompanionWindow).props().title.props.children; - expect(title[0]).toEqual('searchTitle'); - expect(title[1]).toEqual(''); + expect(screen.queryByRole('button', { name: 'clearSearch' })).not.toBeInTheDocument(); }); it('has the SearchPanelControls component', () => { - const titleControls = createWrapper().find(CompanionWindow).prop('titleControls'); - expect(titleControls.type.displayName).toEqual('Connect(WithStyles(WithPlugins(SearchPanelControls)))'); + createWrapper(); + + expect(screen.getByRole('combobox', { name: 'searchInputLabel' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'searchSubmitAria' })).toBeInTheDocument(); }); - it('has the SearchResults', () => { - const wrapper = createWrapper(); - expect(wrapper.find(SearchResults).length).toEqual(1); + + it('has the SearchResults list', () => { + createWrapper(); + + expect(screen.getByRole('list')).toBeInTheDocument(); }); - it('suggests searches', () => { + it('suggests searches', async () => { + const user = userEvent.setup(); const fetchSearch = jest.fn(); - const wrapper = createWrapper({ fetchSearch, suggestedSearches: ['abc'] }); - expect(wrapper.find(Button).length).toEqual(1); - wrapper.find(Button).simulate('click'); + createWrapper({ + fetchSearch, query: '', suggestedSearches: ['abc'], t: i18next.t, + }); + + expect(screen.getByRole('button', { name: 'Search this document for "abc"' })).toBeInTheDocument(); + await user.click(screen.getByRole('button', { name: 'Search this document for "abc"' })); expect(fetchSearch).toHaveBeenCalledWith('http://example.com/search?q=abc', 'abc'); + }); + + it('does not suggest searches if the user has made a query', () => { + const fetchSearch = jest.fn(); + createWrapper({ fetchSearch, query: 'blah', suggestedSearches: ['abc'] }); - wrapper.setProps({ query: 'something' }); - expect(wrapper.find(Button).length).toEqual(0); + expect(screen.queryByRole('button', { name: 'Search this document for "abc"' })).not.toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/SearchPanelControls.test.js b/__tests__/src/components/SearchPanelControls.test.js index aa147c12e2536772f8f5628a7e64eb062570addc..0b018912404980e4266fbf88a25092ac8f518e09 100644 --- a/__tests__/src/components/SearchPanelControls.test.js +++ b/__tests__/src/components/SearchPanelControls.test.js @@ -1,16 +1,13 @@ -import { shallow } from 'enzyme'; -import Autocomplete from '@material-ui/lab/Autocomplete'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import Input from '@material-ui/core/Input'; -import TextField from '@material-ui/core/TextField'; -import SearchIcon from '@material-ui/icons/SearchSharp'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; + import { SearchPanelControls } from '../../../src/components/SearchPanelControls'; /** * Helper function to create a shallow wrapper around AttributionPanel */ function createWrapper(props) { - return shallow( + return render( <SearchPanelControls companionWindowId="cw" windowId="window" @@ -20,145 +17,97 @@ function createWrapper(props) { } describe('SearchPanelControls', () => { - it('renders a form and navigation', () => { - const wrapper = createWrapper(); - expect(wrapper.find('form').length).toEqual(1); - expect(wrapper.find('Connect(WithStyles(WithPlugins(SearchPanelNavigation)))').length).toEqual(1); + it('renders a form', () => { + createWrapper(); + expect(screen.getByRole('form')).toBeInTheDocument(); }); - it('submits a search when an autocomplete suggestion is picked', () => { + it('submits a search when an autocomplete suggestion is picked', async () => { + const user = userEvent.setup(); const fetchSearch = jest.fn(); - const wrapper = createWrapper({ - fetchSearch, - searchService: { id: 'http://example.com/search', options: { resource: { id: 'abc' } } }, - }); - const value = 'somestring'; - wrapper.find(Autocomplete).prop('onChange')({}, { match: value }, {}); - expect(wrapper.state().search).toEqual(value); - expect(fetchSearch).toHaveBeenCalledWith('window', 'cw', 'http://example.com/search?q=somestring', 'somestring'); - }); - it('renders a text input through the renderInput prop', () => { - const wrapper = createWrapper(); - const input = wrapper.find(Autocomplete).dive().dive().find(TextField); - - expect(input.prop('id')).toEqual('search-cw'); - }); - it('endAdornment is a SearchIcon (with no CircularProgress indicator)', () => { - const wrapper = createWrapper(); - const divedInput = wrapper.find(Autocomplete).dive().dive().find(TextField) - .dive() - .dive() - .find(Input) - .dive() - .dive() - .dive() - .dive(); - expect(divedInput.find(CircularProgress).length).toEqual(0); - expect(divedInput.find(SearchIcon).length).toEqual(1); - expect(divedInput.find('Connect(WithPlugins(MiradorMenuButton))[type="submit"]').length).toEqual(1); - }); - - it('endAdornment has a CircularProgress indicator when there the current search is fetching', () => { - const wrapper = createWrapper({ searchIsFetching: true }); - const divedInput = wrapper.find(Autocomplete).dive().dive().find(TextField) - .dive() - .dive() - .find(Input) - .dive() - .dive() - .dive() - .dive(); - expect(divedInput.find(CircularProgress).length).toEqual(1); - }); + fetch.mockResponse(JSON.stringify({ terms: ['somestring 12345'] })); - it('passes the appropriate options prop to Autocomplete based on state', () => { - const fetchSearch = jest.fn(); - const wrapper = createWrapper({ + createWrapper({ + autocompleteService: { id: 'http://example.com/autocomplete' }, fetchSearch, searchService: { id: 'http://example.com/search', options: { resource: { id: 'abc' } } }, }); - wrapper.setState({ search: 'yolo', suggestions: [{ match: 'abc' }] }); - const divedInput = wrapper.find(Autocomplete).dive(); + await user.click(screen.getByRole('combobox')); + await user.keyboard('somestring'); + await user.click(await screen.findByText('somestring 12345')); + expect(fetchSearch).toHaveBeenCalledWith('window', 'cw', 'http://example.com/search?q=somestring+12345', 'somestring 12345'); - expect(divedInput.prop('options')).toEqual([{ match: 'abc' }]); + fetch.resetMocks(); }); - it('updates the search state and submits the search form onChange (when an item is selected)', () => { - const fetchSearch = jest.fn(); - const wrapper = createWrapper({ - fetchSearch, - searchService: { id: 'http://example.com/search', options: { resource: { id: 'abc' } } }, - }); - wrapper.setState({ search: 'yolo', suggestions: [{ match: 'abc' }] }); - - const divedInput = wrapper.find(Autocomplete).dive(); + it('renders a text input through the renderInput prop', () => { + createWrapper(); - divedInput.prop('onChange')({}, { match: 'abc' }, {}); - expect(wrapper.state().search).toBe('abc'); + expect(screen.getByRole('combobox')).toHaveAttribute('id', 'search-cw'); + }); + it('endAdornment is a SearchIcon (with no CircularProgress indicator)', () => { + createWrapper(); + expect(screen.getByRole('button').querySelector('svg')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access + expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); }); - // The MUI Autocomplete renders the suggestions via portal (so not as a child of the component). - // We test the options passed in and the result of the onChange event above, but don't have a test - // simulating a click on a rendered option like we do below. - xit('renders suggestions', () => { - const fetchSearch = jest.fn(); - const wrapper = createWrapper({ - fetchSearch, - searchService: { id: 'http://example.com/search', options: { resource: { id: 'abc' } } }, - }); - wrapper.setState({ search: 'yolo', suggestions: [{ match: 'abc' }] }); + it('endAdornment has a CircularProgress indicator when there the current search is fetching', () => { + createWrapper({ searchIsFetching: true }); - // const divedInput = wrapper.find(Autocomplete).dive(); - // expect(divedInput.find(MenuItem).length).toEqual(1); - // expect(divedInput.find(MenuItem).text()).toEqual('abc'); - // divedInput.find(MenuItem).simulate('click', {}); - expect(wrapper.state().search).toBe('abc'); + expect(screen.getByRole('progressbar')).toBeInTheDocument(); }); - it('form change and submission triggers an action', () => { + it('form change and submission triggers an action', async () => { + const user = userEvent.setup(); const fetchSearch = jest.fn(); const searchService = { id: 'http://www.example.com/search', options: { resource: { id: 'example.com/manifest' } }, }; - const wrapper = createWrapper({ fetchSearch, searchService }); - wrapper.setState({ search: 'asdf' }); + createWrapper({ fetchSearch, query: 'asdf', searchService }); - wrapper.setState({ search: 'yolo' }); + await user.clear(screen.getByRole('combobox')); + await user.keyboard('yolo'); + await user.click(screen.getByRole('button')); - wrapper.find('form').simulate('submit', { preventDefault: () => {} }); expect(fetchSearch).toHaveBeenCalledWith('window', 'cw', 'http://www.example.com/search?q=yolo', 'yolo'); - expect(wrapper.state().search).toBe('yolo'); }); - it('does not submit an empty search', () => { + it('does not submit an empty search', async () => { + const user = userEvent.setup(); const fetchSearch = jest.fn(); const searchService = { id: 'http://www.example.com/search', options: { resource: { id: 'example.com/manifest' } }, }; - const wrapper = createWrapper({ fetchSearch, searchService }); - wrapper.setState({ search: '' }); - wrapper.find('form').simulate('submit', { preventDefault: () => {} }); + createWrapper({ fetchSearch, query: '', searchService }); + + await user.clear(screen.getByRole('combobox')); + await user.click(screen.getByRole('button', { name: 'searchSubmitAria' })); expect(fetchSearch).not.toHaveBeenCalled(); }); describe('input', () => { it('has the query prop has the input value on intial render', () => { - const wrapper = createWrapper({ query: 'Wolpertinger' }); - - expect(wrapper.find(Autocomplete).props().inputValue).toEqual('Wolpertinger'); + createWrapper({ query: 'Wolpertinger' }); + expect(screen.getByRole('combobox')).toHaveValue('Wolpertinger'); }); it('clears the local search state/input when the incoming query prop has been cleared', () => { const wrapper = createWrapper({ query: 'Wolpertinger' }); + expect(screen.getByRole('combobox')).toHaveValue('Wolpertinger'); + + wrapper.rerender(( + <SearchPanelControls + companionWindowId="cw" + windowId="window" + query="" + /> + )); - expect(wrapper.state().search).toEqual('Wolpertinger'); - wrapper.setProps({ query: '' }); - expect(wrapper.state().search).toEqual(''); - expect(wrapper.find(Autocomplete).props().inputValue).toEqual(''); + expect(screen.getByRole('combobox')).toHaveValue(''); }); }); }); diff --git a/__tests__/src/components/SearchPanelNavigation.test.js b/__tests__/src/components/SearchPanelNavigation.test.js index 50b005fdffa916d5ebc2660a891c7551b7149768..f4690e82bcbba0a186da6d904a3ffd49b0fb929f 100644 --- a/__tests__/src/components/SearchPanelNavigation.test.js +++ b/__tests__/src/components/SearchPanelNavigation.test.js @@ -1,11 +1,12 @@ -import { shallow } from 'enzyme'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { SearchPanelNavigation } from '../../../src/components/SearchPanelNavigation'; /** * Helper function to create a shallow wrapper around SearchPanelNavigation */ function createWrapper(props) { - return shallow( + return render( <SearchPanelNavigation companionWindowId="cw" direction="ltr" @@ -17,26 +18,28 @@ function createWrapper(props) { describe('SearchPanelNavigation', () => { describe('when searchHits are available', () => { - it('renders text with buttons', () => { + it('renders text with buttons', async () => { const selectAnnotation = jest.fn(); - const wrapper = createWrapper({ + const user = userEvent.setup(); + createWrapper({ searchHits: [{ annotations: ['1'] }, { annotations: ['2'] }, { annotations: ['3'] }], selectAnnotation, selectedContentSearchAnnotation: ['2'], }); - expect(wrapper.find('WithStyles(ForwardRef(Typography))').text()).toEqual('pagination'); - expect(wrapper.find('Connect(WithPlugins(MiradorMenuButton))[disabled=false]').length).toEqual(2); - wrapper.find('Connect(WithPlugins(MiradorMenuButton))[disabled=false]').first().props().onClick(); + expect(screen.getByText('pagination')).toBeInTheDocument(); + expect(screen.getAllByRole('button').length).toEqual(2); + await user.click(screen.getByRole('button', { name: 'searchPreviousResult' })); expect(selectAnnotation).toHaveBeenCalledWith('1'); - wrapper.find('Connect(WithPlugins(MiradorMenuButton))[disabled=false]').last().props().onClick(); + await user.click(screen.getByRole('button', { name: 'searchNextResult' })); expect(selectAnnotation).toHaveBeenCalledWith('3'); }); it('buttons disabled when no next/prev', () => { - const wrapper = createWrapper({ + createWrapper({ searchHits: [{ annotations: ['1'] }], selectedContentSearchAnnotation: ['1'], }); - expect(wrapper.find('Connect(WithPlugins(MiradorMenuButton))[disabled=true]').length).toEqual(2); + expect(screen.getByRole('button', { name: 'searchPreviousResult' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'searchNextResult' })).toBeDisabled(); }); }); }); diff --git a/__tests__/src/components/SearchResults.test.js b/__tests__/src/components/SearchResults.test.js index ce0603bc1d00425b3d99e78b9f32a8bbd637cca2..3579a9789afeea5b5de2df963e75b981cfb92fb4 100644 --- a/__tests__/src/components/SearchResults.test.js +++ b/__tests__/src/components/SearchResults.test.js @@ -1,13 +1,13 @@ -import { shallow } from 'enzyme'; -import Button from '@material-ui/core/Button'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; + import { SearchResults } from '../../../src/components/SearchResults'; -import { ScrollTo } from '../../../src/components/ScrollTo'; /** * Helper function to create a shallow wrapper around SearchResults */ function createWrapper(props) { - return shallow( + return render( <SearchResults companionWindowId="cwid" windowId="window" @@ -23,105 +23,123 @@ function createWrapper(props) { ]} {...props} />, + { + preloadedState: { + companionWindows: { + cwid: {}, + }, + searches: { + window: { + cwid: { + data: { + 'http://example.com/contentsearch': { + id: 'http://example.com/contentsearch', + isFetching: false, + json: { + resources: [ + { + '@id': 'foo', + '@type': 'oa:Annotation', + motivation: 'sc:painting', + }, + { + '@id': 'x', + label: 'The Anno Label', + }, + { + '@id': 'y', + label: 'Annother Anno Label', + }, + ], + }, + }, + }, + query: '', + selectedContentSearchAnnotation: null, + selectedContentSearchAnnotationIds: [], + }, + }, + }, + windows: { + window: {}, + }, + }, + }, ); } describe('SearchResults', () => { it('renders a SearchHit for each hit', () => { - const selectContentSearchAnnotation = jest.fn(); - const wrapper = createWrapper({ selectContentSearchAnnotation }); - const searchHits = wrapper.find('LiveMessenger').props().children({}); + createWrapper({}); - expect(searchHits.length).toEqual(1); - expect(searchHits[0].type.displayName).toEqual('Connect(WithStyles(WithPlugins(SearchHit)))'); - expect(searchHits[0].props.index).toEqual(0); + expect(screen.getByRole('button', { name: /Light up the moose/ })).toBeInTheDocument(); }); - it('can focus on a single item', () => { - const wrapper = createWrapper({}); - const searchHits = wrapper.find('LiveMessenger').props().children({}); + it('can focus on a single item', async () => { + const user = userEvent.setup(); + createWrapper({}); - searchHits[0].props.showDetails(); - expect(wrapper.state().focused).toEqual(true); - }); + await user.click(screen.getByRole('button', { name: 'more' })); + expect(screen.getByRole('listitem')).toHaveTextContent(/start the chainsaw/); - it('can return to the full list view', () => { - const wrapper = createWrapper({}); - wrapper.setState({ focused: true }); - expect(wrapper.find(ScrollTo).prop('scrollTo')).toEqual(true); - expect(wrapper.find(Button).text()).toEqual('backToResults'); - wrapper.find(Button).simulate('click'); - expect(wrapper.state().focused).toEqual(false); - }); - - it('passes announcePolite function to the SearchHits', () => { - const announcePolite = jest.fn(); - const wrapper = createWrapper({}); - const searchHits = wrapper.find('LiveMessenger').props().children({ announcePolite }); + expect(screen.queryByRole('button', { name: 'more' })).not.toBeInTheDocument(); - expect(searchHits[0].props.announcer).toEqual(announcePolite); + await user.click(screen.getByRole('button', { name: 'backToResults' })); + expect(screen.getByRole('button', { name: 'more' })).toBeInTheDocument(); }); describe('annotation-only search results', () => { it('renders a SearchHit for each annotation', () => { - const wrapper = createWrapper({ + createWrapper({ searchAnnotations: [{ id: 'x' }, { id: 'y' }], searchHits: [], }); - const searchHits = wrapper.find('LiveMessenger').props().children({}); - expect(searchHits.length).toEqual(2); - expect(searchHits[0].props.index).toEqual(0); - expect(searchHits[0].props.annotationId).toEqual('x'); - - expect(searchHits[1].props.index).toEqual(1); - expect(searchHits[1].props.annotationId).toEqual('y'); + expect(screen.getByRole('heading', { level: 4, name: 'The Anno Label' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 4, name: 'Annother Anno Label' })).toBeInTheDocument(); }); }); describe('no search results', () => { it('shows no results', () => { - const wrapper = createWrapper({ + createWrapper({ isFetching: false, query: 'nope', searchHits: [], }); - expect(wrapper.find('WithStyles(ForwardRef(Typography))').text()).toEqual('searchNoResults'); - }); - it('with hits', () => { - const wrapper = createWrapper({ - isFetching: false, - query: 'nope', - }); - expect(wrapper.find('WithStyles(ForwardRef(Typography))').length).toEqual(0); + + expect(screen.getByText('searchNoResults')).toHaveClass('MuiTypography-body1'); }); + it('while fetching', () => { - const wrapper = createWrapper({ + createWrapper({ isFetching: true, query: 'nope', + searchHits: [], }); - expect(wrapper.find('WithStyles(ForwardRef(Typography))').length).toEqual(0); + + expect(screen.queryByText('searchNoResults')).not.toBeInTheDocument(); }); + it('without a query', () => { - const wrapper = createWrapper({ + createWrapper({ isFetching: false, query: '', }); - expect(wrapper.find('WithStyles(ForwardRef(Typography))').length).toEqual(0); + expect(screen.queryByText('searchNoResults')).not.toBeInTheDocument(); }); }); describe('multi-page search results', () => { - it('shows a button to request the next page', () => { + it('shows a button to request the next page', async () => { + const user = userEvent.setup(); const fetchSearch = jest.fn(); - const wrapper = createWrapper({ + createWrapper({ fetchSearch, nextSearch: 'search?page=2', }); - expect(wrapper.find(Button).length).toEqual(1); - wrapper.find(Button).simulate('click'); - + await user.click(screen.getByRole('button', { name: /moreResults/ })); expect(fetchSearch).toHaveBeenCalledWith('window', 'cwid', 'search?page=2', 'query'); }); }); diff --git a/__tests__/src/components/SidebarIndexItem.test.js b/__tests__/src/components/SidebarIndexItem.test.js index 23adb0a16ed2f4588be62307fcee24ce1c739872..7872043b7bc9fb310c295b9e6b98ae21aa53a26d 100644 --- a/__tests__/src/components/SidebarIndexItem.test.js +++ b/__tests__/src/components/SidebarIndexItem.test.js @@ -1,10 +1,9 @@ -import { shallow } from 'enzyme'; -import Typography from '@material-ui/core/Typography'; +import { render, screen } from 'test-utils'; import { SidebarIndexItem } from '../../../src/components/SidebarIndexItem'; /** */ function createWrapper(props) { - return shallow( + return render( <SidebarIndexItem label="yolo" classes={{}} @@ -15,8 +14,7 @@ function createWrapper(props) { describe('SidebarIndexItem', () => { it('creates Typography with a canvas label', () => { - const wrapper = createWrapper(); - expect(wrapper.find(Typography).length).toBe(1); - expect(wrapper.text()).toBe('yolo'); + createWrapper(); + expect(screen.getByText('yolo')).toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/SidebarIndexList.test.js b/__tests__/src/components/SidebarIndexList.test.js index b65bbab101bfc2016271b46f73fc300b0fcaac71..e86e60fea3f022e8645470640ef0e93bd392c604 100644 --- a/__tests__/src/components/SidebarIndexList.test.js +++ b/__tests__/src/components/SidebarIndexList.test.js @@ -1,9 +1,8 @@ -import { shallow } from 'enzyme'; -import MenuList from '@material-ui/core/MenuList'; -import MenuItem from '@material-ui/core/MenuItem'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { Utils } from 'manifesto.js'; + import { SidebarIndexList } from '../../../src/components/SidebarIndexList'; -import SidebarIndexItem from '../../../src/containers/SidebarIndexItem'; import manifestJson from '../../fixtures/version-2/019.json'; /** @@ -12,7 +11,7 @@ import manifestJson from '../../fixtures/version-2/019.json'; function createWrapper(props) { const canvases = Utils.parseManifest(manifestJson).getSequences()[0].getCanvases(); - return shallow( + return render( <SidebarIndexList id="asdf" canvases={canvases} @@ -36,57 +35,29 @@ describe('SidebarIndexList', () => { }); it('renders all needed elements for the thumbnail view', () => { - const wrapper = createWrapper(); - expect(wrapper.find(MenuList).length).toBe(1); - expect(wrapper.find(MenuItem).length).toBe(3); - expect(wrapper.find(MenuItem).first().props().component).toEqual('li'); - expect(wrapper.find(MenuItem).at(1).props().selected).toBe(true); - expect(wrapper.find(MenuList).find(SidebarIndexItem).length).toBe(3); + const { container } = createWrapper({ variant: 'thumbnail' }); + + expect(screen.getByRole('menuitem', { name: 'Test 19 Canvas: 1' })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: 'Image 1' })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: 'Image 2' })).toBeInTheDocument(); + + expect(container.querySelectorAll('img').length).toBe(3); // eslint-disable-line testing-library/no-node-access, testing-library/no-container + + expect(screen.getByRole('menuitem', { name: 'Image 1' })).toHaveClass('Mui-selected'); }); it('renders all needed elements for the item view', () => { - const wrapper = createWrapper({ variant: 'item' }); - expect(wrapper.find(MenuList).length).toBe(1); - expect(wrapper.find(MenuItem).length).toBe(3); - expect(wrapper.find(MenuItem).first().props().component).toEqual('li'); - expect(wrapper.find(MenuList).find(SidebarIndexItem).length).toBe(3); - }); + createWrapper({ variant: 'item' }); - it('should call the onClick handler of a list item', () => { - const wrapper = createWrapper({ setCanvas }); - wrapper.find(MenuItem).at(1).simulate('click'); - expect(setCanvas).toHaveBeenCalledTimes(1); + expect(screen.getAllByRole('menuitem').length).toBe(3); + expect(screen.getByRole('menuitem', { name: 'Image 1' })).toHaveClass('Mui-selected'); }); - describe('getIdAndLabelOfCanvases', () => { - it('should return id and label of each canvas in manifest', () => { - const canvases = Utils - .parseManifest(manifestJson) - .getSequences()[0] - .getCanvases(); - const wrapper = createWrapper({ canvases }); - const received = wrapper.instance().getIdAndLabelOfCanvases(canvases); - const expected = [ - { - id: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json', - label: 'Test 19 Canvas: 1', - }, - { - id: 'https://purl.stanford.edu/fr426cg9537/iiif/canvas/fr426cg9537_1', - label: 'Image 1', - }, - { - id: 'https://purl.stanford.edu/rz176rt6531/iiif/canvas/rz176rt6531_1', - label: 'Image 2', - }, - ]; - expect(received).toEqual(expected); - }); + it('should call the onClick handler of a list item', async () => { + const user = userEvent.setup(); + createWrapper({ setCanvas }); + await user.click(screen.getByRole('menuitem', { name: 'Image 2' })); - it('should return empty array if canvas if empty', () => { - const wrapper = createWrapper({ canvases: [] }); - const received = wrapper.instance().getIdAndLabelOfCanvases([]); - expect(received).toEqual([]); - }); + expect(setCanvas).toHaveBeenCalledWith('xyz', 'https://purl.stanford.edu/rz176rt6531/iiif/canvas/rz176rt6531_1'); }); }); diff --git a/__tests__/src/components/SidebarIndexTableOfContents.test.js b/__tests__/src/components/SidebarIndexTableOfContents.test.js index 76565846a2960586c54d8b98aee93a4b24f9253d..af3f9885db3ac6a8d6fac4cffec9250b34482cd6 100644 --- a/__tests__/src/components/SidebarIndexTableOfContents.test.js +++ b/__tests__/src/components/SidebarIndexTableOfContents.test.js @@ -1,18 +1,19 @@ -import { shallow } from 'enzyme'; +import { render, screen, waitFor } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { Utils } from 'manifesto.js'; -import TreeItem from '@material-ui/lab/TreeItem'; -import TreeView from '@material-ui/lab/TreeView'; + import { SidebarIndexTableOfContents } from '../../../src/components/SidebarIndexTableOfContents'; +import ConnectedSidebarIndexTableOfContents from '../../../src/containers/SidebarIndexTableOfContents'; import manifestVersion2 from '../../fixtures/version-2/structures.json'; import manifestVersion3 from '../../fixtures/version-3/structures.json'; /** - * Create shallow enzyme wrapper for SidebarIndexTableOfContents component + * Create wrapper for SidebarIndexTableOfContents component * @param {*} props */ function createWrapper(props) { const manifest = Utils.parseManifest(props.manifest ? props.manifest : manifestVersion2); - return shallow( + return render( <SidebarIndexTableOfContents id="something" classes={{}} @@ -27,37 +28,59 @@ function createWrapper(props) { } /** - * Create necessary props to simulate keydown event with specific key + * Create an interactive wrapper for SidebarIndexTableOfContents component hooked up + * to the redux store (because the controlled treeview really needs to be connected to + * write a reasonable test for it) */ -function createKeydownProps(key) { - return [ - 'keydown', +function createInteractiveWrapper({ manifest = manifestVersion3, ...props }) { + return render( + <ConnectedSidebarIndexTableOfContents + id="something" + windowId="a" + {...props} + />, { - key, + preloadedState: { + companionWindows: { + something: { + id: 'something', + }, + }, + manifests: { + 'http://example.com/manifest.json': { + json: manifest, + }, + }, + windows: { + a: { manifestId: 'http://example.com/manifest.json' }, + }, + }, }, - ]; + ); } describe('SidebarIndexTableOfContents', () => { - let toggleNode; let setCanvas; beforeEach(() => { - toggleNode = jest.fn(); setCanvas = jest.fn(); }); it('does not render a TreeView if the tree structure is missing', () => { - const wrapper = createWrapper({ + const { container } = createWrapper({ treeStructure: undefined, }); - expect(wrapper.children().length).toBe(0); + + expect(container).toBeEmptyDOMElement(); }); - it('renders a tree item for every node', () => { - const structuresWrapper = createWrapper({}); - expect(structuresWrapper.find(TreeItem)).toHaveLength(21); - const simpleTreeWrapper = createWrapper({ + it('renders a tree item for every visible node', () => { + const { unmount } = createWrapper({}); + expect(screen.getAllByRole('treeitem')).toHaveLength(5); + + unmount(); + + createWrapper({ treeStructure: { nodes: [ { @@ -76,180 +99,134 @@ describe('SidebarIndexTableOfContents', () => { ], }, }); - expect(simpleTreeWrapper.find(TreeItem)).toHaveLength(3); + expect(screen.getByRole('treeitem')).toBeInTheDocument(); }); - it('accepts missing nodes property for tress structure and tree nodes', () => { - const noNodesWrapper = createWrapper({ + it('accepts missing nodes property for tree structure and tree nodes', () => { + const { unmount } = createWrapper({ treeStructure: { nodes: undefined }, }); - expect(noNodesWrapper.find(TreeItem)).toHaveLength(0); - const noChildNodesWrapper = createWrapper({ + expect(screen.queryByRole('treeitem')).not.toBeInTheDocument(); + unmount(); + createWrapper({ treeStructure: { nodes: [{ id: '0' }], }, }); - expect(noChildNodesWrapper.find(TreeItem)).toHaveLength(1); + expect(screen.getByRole('treeitem')).toBeInTheDocument(); }); - it('toggles branch nodes on click, but not leaf nodes', () => { - const wrapper = createWrapper({ setCanvas, toggleNode }); - const treeView = wrapper.find(TreeView); - const node0 = treeView.childAt(0).childAt(0); - expect(node0.prop('nodeId')).toBe('0-0'); - node0.simulate('click'); - node0.simulate('click'); - expect(toggleNode).toHaveBeenCalledTimes(2); - - const node00 = node0.children().at(0).childAt(0); - expect(node00.prop('nodeId')).toBe('0-0-0'); - node00.simulate('click'); - node00.simulate('click'); - expect(toggleNode).toHaveBeenCalledTimes(2); - - const node1 = treeView.childAt(1).childAt(0); - expect(node1.prop('nodeId')).toBe('0-1'); - node1.simulate('click'); - expect(toggleNode).toHaveBeenCalledTimes(3); - }); + it('toggles branch nodes on click', async () => { + const user = userEvent.setup(); + const { store } = createInteractiveWrapper({}); - it('collapses branch nodes (i.e. toggles open branch nodes) with left arrow key', () => { - const wrapper = createWrapper({ - expandedNodeIds: ['0-0'], - setCanvas, - toggleNode, - }); - const treeView = wrapper.find(TreeView); + expect(screen.getByRole('treeitem')).toBeInTheDocument(); + const root = screen.getByRole('treeitem'); - const node0 = treeView.childAt(0).childAt(0); - expect(node0.prop('nodeId')).toBe('0-0'); - node0.simulate(...createKeydownProps('ArrowLeft')); - expect(toggleNode).toHaveBeenCalledTimes(1); + await user.click(root.querySelector('.MuiTreeItem-iconContainer')); // eslint-disable-line testing-library/no-node-access + expect(screen.getAllByRole('treeitem')).toHaveLength(5); - const node00 = node0.children().at(0).childAt(0); - expect(node00.prop('nodeId')).toBe('0-0-0'); - const node1 = treeView.childAt(1).childAt(0); - expect(node1.prop('nodeId')).toBe('0-1'); + await user.click(root.querySelector('.MuiTreeItem-iconContainer')); // eslint-disable-line testing-library/no-node-access - node00.simulate(...createKeydownProps('ArrowLeft')); - node1.simulate(...createKeydownProps('ArrowLeft')); - expect(toggleNode).toHaveBeenCalledTimes(1); + await waitFor(() => { + expect(screen.getByRole('treeitem')).toBeInTheDocument(); + }); + expect(store.getState().windows.a.canvasId).toBeUndefined(); }); - it('expands branch nodes (i.e. toggles closed branch nodes) with right arrow key', () => { - const wrapper = createWrapper({ - expandedNodeIds: ['0-0'], - setCanvas, - toggleNode, + it('toggles branch nodes with arrow keys', async () => { + const user = userEvent.setup(); + + const { store } = createInteractiveWrapper({}); + const root = screen.getByRole('treeitem'); + + root.focus(); + await user.keyboard('{ArrowRight}'); + expect(screen.getAllByRole('treeitem')).toHaveLength(5); + await user.keyboard('{ArrowLeft}'); + await waitFor(() => { + expect(screen.getByRole('treeitem')).toBeInTheDocument(); }); - const treeView = wrapper.find(TreeView); - const node0 = treeView.childAt(0).childAt(0); - expect(node0.prop('nodeId')).toBe('0-0'); - const node00 = node0.children().at(0).childAt(0); - expect(node00.prop('nodeId')).toBe('0-0-0'); - - node0.simulate(...createKeydownProps('ArrowRight')); - node00.simulate(...createKeydownProps('ArrowRight')); - expect(toggleNode).toHaveBeenCalledTimes(0); - - const node1 = treeView.childAt(1).childAt(0); - expect(node1.prop('nodeId')).toBe('0-1'); - node1.simulate(...createKeydownProps('ArrowRight')); - expect(toggleNode).toHaveBeenCalledTimes(1); + + expect(store.getState().windows.a.canvasId).toBeUndefined(); }); - it('toggles branch nodes (but not leaf nodes) with Space or Enter key', () => { - const wrapper = createWrapper({ setCanvas, toggleNode }); - const treeView = wrapper.find(TreeView); - const node0 = treeView.childAt(0).childAt(0); - node0.simulate(...createKeydownProps('Enter')); - expect(toggleNode).toHaveBeenCalledTimes(1); - node0.simulate(...createKeydownProps(' ')); - expect(toggleNode).toHaveBeenCalledTimes(2); - node0.simulate(...createKeydownProps('Spacebar')); - expect(toggleNode).toHaveBeenCalledTimes(3); - node0.simulate(...createKeydownProps('Tab')); - node0.children().at(0).simulate(...createKeydownProps('Enter')); - node0.children().at(0).simulate(...createKeydownProps(' ')); - expect(toggleNode).toHaveBeenCalledTimes(3); - treeView.childAt(1).childAt(0).simulate(...createKeydownProps('Enter')); - treeView.childAt(1).childAt(0).simulate(...createKeydownProps(' ')); - expect(toggleNode).toHaveBeenCalledTimes(5); + it('toggles branch nodes with Space or Enter key', async () => { + const user = userEvent.setup(); + + const { store } = createInteractiveWrapper({}); + const root = screen.getByRole('treeitem'); + + root.focus(); + await user.keyboard('{Enter}'); + expect(screen.getAllByRole('treeitem')).toHaveLength(5); + await user.keyboard('{ArrowLeft}'); + await waitFor(() => { + expect(screen.getByRole('treeitem')).toBeInTheDocument(); + }); + await user.keyboard(' '); + expect(screen.getAllByRole('treeitem')).toHaveLength(5); + + expect(store.getState().windows.a.canvasId).toBeUndefined(); }); - it('calls setCanvas only on click for ranges with canvases that do not have children', () => { - const wrapper = createWrapper({ setCanvas, toggleNode }); - const treeView = wrapper.find(TreeView); - const node0 = treeView.childAt(0).childAt(0); - expect(node0.prop('nodeId')).toBe('0-0'); - node0.simulate('click'); - expect(setCanvas).toHaveBeenCalledTimes(0); - node0.childAt(0).childAt(0).simulate('click'); - expect(setCanvas).toHaveBeenCalledTimes(1); - node0.childAt(1).childAt(0).simulate('click'); - expect(setCanvas).toHaveBeenCalledTimes(2); - node0.childAt(2).childAt(0).simulate('click'); - expect(setCanvas).toHaveBeenCalledTimes(3); - - const node1 = treeView.childAt(1).childAt(0); - expect(node1.prop('nodeId')).toBe('0-1'); - node1.simulate(...createKeydownProps('ArrowRight')); - expect(setCanvas).toHaveBeenCalledTimes(3); + it('calls setCanvas only on click for leaf nodes', async () => { + const user = userEvent.setup(); + + const { store } = createInteractiveWrapper({}); + const root = screen.getByRole('treeitem'); + + root.focus(); + await user.keyboard('{Enter}'); + + const leafNode = screen.getAllByRole('treeitem')[1]; + + leafNode.focus(); + await user.keyboard('{Enter}'); + + expect(store.getState().windows.a.canvasId).toEqual('http://foo.test/1/canvas/c2'); }); - it('sets the canvas to a start canvas if present', () => { - const version2wrapper = createWrapper({ + it('sets the canvas to a start canvas if present (IIIF v2)', async () => { + const user = userEvent.setup(); + createWrapper({ + expandNodes: () => { }, + manifest: manifestVersion2, setCanvas, - toggleNode, - windowId: 'w1', + windowId: 'a', }); - const treeView = version2wrapper.find(TreeView); - const node3 = treeView.childAt(3).childAt(0); - expect(node3.prop('nodeId')).toBe('0-3'); - node3.simulate('click'); - expect(setCanvas).toHaveBeenCalledWith('w1', 'http://foo.test/1/canvas/c11'); - const version3wrapper = createWrapper({ + const leafNode = screen.getAllByRole('treeitem')[3]; + leafNode.focus(); + await user.keyboard('{Enter}'); + + expect(setCanvas).toHaveBeenLastCalledWith('a', 'http://foo.test/1/canvas/c11'); + }); + + it('sets the canvas to a start canvas if present (IIIF v3)', async () => { + const user = userEvent.setup(); + + const { store } = createInteractiveWrapper({ manifest: manifestVersion3, - setCanvas, - toggleNode, - windowId: 'w1', }); - const treeViewVersion3 = version3wrapper.find(TreeView); - const rootNode = treeViewVersion3.childAt(0).childAt(0); - const version3node1 = rootNode.childAt(1).childAt(0); - expect(version3node1.prop('nodeId')).toBe('0-0-1'); - version3node1.simulate('click'); - expect(setCanvas).toHaveBeenLastCalledWith('w1', 'http://foo.test/1/canvas/c7'); - - const version3node2 = rootNode.childAt(2).childAt(0); - expect(version3node2.prop('nodeId')).toBe('0-0-2'); - version3node2.simulate('click'); - expect(setCanvas).toHaveBeenLastCalledWith('w1', 'http://foo.test/1/canvas/c9'); - - const version3node3 = rootNode.childAt(3).childAt(0); - expect(version3node3.prop('nodeId')).toBe('0-0-3'); - version3node3.simulate('click'); - expect(setCanvas).toHaveBeenLastCalledWith('w1', 'http://foo.test/1/canvas/c10'); - }); - it('does not select a canvas when opening a node with the right arrow key', () => { - const wrapper = createWrapper({ setCanvas, toggleNode }); - const treeView = wrapper.find(TreeView); - const node0 = treeView.childAt(0).childAt(0); - expect(node0.prop('nodeId')).toBe('0-0'); - node0.simulate(...createKeydownProps('ArrowRight')); - expect(setCanvas).toHaveBeenCalledTimes(0); - expect(toggleNode).toHaveBeenCalledTimes(1); - }); + const root = screen.getByRole('treeitem'); + root.focus(); + await user.keyboard('{Enter}'); + + const leafNode1 = screen.getAllByRole('treeitem')[2]; + leafNode1.focus(); + await user.keyboard('{Enter}'); + expect(store.getState().windows.a.canvasId).toEqual('http://foo.test/1/canvas/c7'); + + const leafNode2 = screen.getAllByRole('treeitem')[3]; + leafNode2.focus(); + await user.keyboard('{Enter}'); + expect(store.getState().windows.a.canvasId).toEqual('http://foo.test/1/canvas/c9'); - it('does not select a canvas when closing a node with the left arrow key', () => { - const wrapper = createWrapper({ expandedNodeIds: ['0-0'], setCanvas, toggleNode }); - const treeView = wrapper.find(TreeView); - const node0 = treeView.childAt(0).childAt(0); - expect(node0.prop('nodeId')).toBe('0-0'); - node0.simulate(...createKeydownProps('ArrowLeft')); - expect(setCanvas).toHaveBeenCalledTimes(0); - expect(toggleNode).toHaveBeenCalledTimes(1); + const leafNode3 = screen.getAllByRole('treeitem')[4]; + leafNode3.focus(); + await user.keyboard('{Enter}'); + expect(store.getState().windows.a.canvasId).toEqual('http://foo.test/1/canvas/c10'); }); }); diff --git a/__tests__/src/components/SidebarIndexThumbnail.test.js b/__tests__/src/components/SidebarIndexThumbnail.test.js index b2e3563966c9ace7f6acb51f90f696c83568a88a..81b004f31bbc7dc0b46cd2ed50b78728c29360fe 100644 --- a/__tests__/src/components/SidebarIndexThumbnail.test.js +++ b/__tests__/src/components/SidebarIndexThumbnail.test.js @@ -1,13 +1,12 @@ -import { shallow } from 'enzyme'; -import Typography from '@material-ui/core/Typography'; +import { render, screen } from 'test-utils'; import { Utils } from 'manifesto.js'; + import fixture from '../../fixtures/version-2/019.json'; import { SidebarIndexThumbnail } from '../../../src/components/SidebarIndexThumbnail'; -import IIIFThumbnail from '../../../src/containers/IIIFThumbnail'; /** */ function createWrapper(props) { - return shallow( + return render( <SidebarIndexThumbnail canvas={Utils.parseManifest(fixture).getSequences()[0].getCanvases()[1]} label="yolo" @@ -20,12 +19,14 @@ function createWrapper(props) { describe('SidebarIndexThumbnail', () => { it('creates Typography with a canvas label', () => { - const wrapper = createWrapper(); - expect(wrapper.find(Typography).length).toBe(1); - expect(wrapper.text()).toEqual(expect.stringContaining('yolo')); + createWrapper(); + + expect(screen.getByText('yolo', { container: 'p' })).toBeInTheDocument(); }); + it('contains a IIIFThumbnail', () => { - const wrapper = createWrapper(); - expect(wrapper.find(IIIFThumbnail).length).toBe(1); + const { container } = createWrapper(); + + expect(container.querySelector('img')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access, testing-library/no-container }); }); diff --git a/__tests__/src/components/ThumbnailCanvasGrouping.test.js b/__tests__/src/components/ThumbnailCanvasGrouping.test.js index 1442a8784d1736b37e26cfd96cf98bc5049514ea..8bfff1d6d06dc63f5b0ff8a348b4eb52c744e72d 100644 --- a/__tests__/src/components/ThumbnailCanvasGrouping.test.js +++ b/__tests__/src/components/ThumbnailCanvasGrouping.test.js @@ -1,19 +1,21 @@ -import { shallow } from 'enzyme'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { Utils } from 'manifesto.js'; + import { ThumbnailCanvasGrouping } from '../../../src/components/ThumbnailCanvasGrouping'; -import IIIFThumbnail from '../../../src/containers/IIIFThumbnail'; import CanvasGroupings from '../../../src/lib/CanvasGroupings'; import manifestJson from '../../fixtures/version-2/019.json'; /** create wrapper */ function createWrapper(props) { - return shallow( + return render( <ThumbnailCanvasGrouping index={1} currentCanvasId="https://purl.stanford.edu/fr426cg9537/iiif/canvas/fr426cg9537_1" classes={{}} style={{ height: 90, + top: 0, width: 100, }} {...props} @@ -23,7 +25,6 @@ function createWrapper(props) { describe('ThumbnailCanvasGrouping', () => { let wrapper; - let rightWrapper; let setCanvas; const data = { canvasGroupings: new CanvasGroupings(Utils.parseManifest(manifestJson) @@ -35,33 +36,37 @@ describe('ThumbnailCanvasGrouping', () => { setCanvas = jest.fn(); wrapper = createWrapper({ data, setCanvas }); }); - it('renders', () => { - expect(wrapper.find('.mirador-thumbnail-nav-container').length).toEqual(1); + const spyCurrentCanvasClass = jest.spyOn(ThumbnailCanvasGrouping.prototype, 'currentCanvasClass'); + afterEach(() => { + spyCurrentCanvasClass.mockClear(); }); - it('sets a mirador-current-canvas-grouping class on current canvas', () => { - expect(wrapper.find('.mirador-thumbnail-nav-canvas-1.mirador-current-canvas-grouping').length).toEqual(1); + it('renders', () => { + expect(screen.getByRole('gridcell')).toBeInTheDocument(); }); it('renders a CaptionedIIIFThumbnail', () => { - expect(wrapper.find(IIIFThumbnail).length).toEqual(1); + expect(screen.getByText('Image 1')).toBeInTheDocument(); }); - it('when clicked, updates the current canvas', () => { + it('when clicked, updates the current canvas', async () => { + wrapper.unmount(); + const user = userEvent.setup(); wrapper = createWrapper({ data, index: 0, setCanvas }); - wrapper.find('.mirador-thumbnail-nav-canvas-0').simulate('click', { currentTarget: { dataset: { canvasId: 'info:0' } } }); - expect(setCanvas).toHaveBeenCalledWith('info:0'); + await user.click(wrapper.container.querySelector('.mirador-thumbnail-nav-canvas-0')); // eslint-disable-line testing-library/no-node-access + expect(spyCurrentCanvasClass).toHaveBeenCalledWith([0]); + expect(spyCurrentCanvasClass).toHaveReturnedWith('current-canvas-grouping'); + expect(setCanvas).toHaveBeenCalledWith('http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json'); }); describe('attributes based off far-bottom position', () => { it('in button div', () => { - expect(wrapper.find('.mirador-thumbnail-nav-canvas').first().props().style).toEqual( - expect.objectContaining({ - height: '123px', - width: 'auto', - }), - ); + expect(screen.getByRole('button', { name: 'Image 1' })).toHaveStyle({ + height: '123px', + width: 'auto', + }); }); }); describe('attributes based off far-right position', () => { beforeEach(() => { - rightWrapper = createWrapper({ + wrapper.unmount(); + createWrapper({ data: { ...data, position: 'far-right', @@ -70,12 +75,10 @@ describe('ThumbnailCanvasGrouping', () => { }); }); it('in button div', () => { - expect(rightWrapper.find('.mirador-thumbnail-nav-canvas').first().props().style).toEqual( - expect.objectContaining({ - height: 'auto', - width: '100px', - }), - ); + expect(screen.getByRole('button', { name: 'Image 1' })).toHaveStyle({ + height: 'auto', + width: '100px', + }); }); }); }); diff --git a/__tests__/src/components/ThumbnailNavigation.test.js b/__tests__/src/components/ThumbnailNavigation.test.js index cfeaa462183044dccec9692c47ae8701136c2688..bf7c89ae4d98c30fafebda31ce4b4aacfb20d4e6 100644 --- a/__tests__/src/components/ThumbnailNavigation.test.js +++ b/__tests__/src/components/ThumbnailNavigation.test.js @@ -1,20 +1,21 @@ -import { shallow } from 'enzyme'; +import { render, screen, fireEvent } from 'test-utils'; +import { PropTypes } from 'prop-types'; import { Utils } from 'manifesto.js'; + import { ThumbnailNavigation } from '../../../src/components/ThumbnailNavigation'; -import ThumbnailCanvasGrouping from '../../../src/containers/ThumbnailCanvasGrouping'; import CanvasGroupings from '../../../src/lib/CanvasGroupings'; import manifestJson from '../../fixtures/version-2/019.json'; import zeroWidthFixture from '../../fixtures/version-2/zeroWidthCanvas.json'; -/** create wrapper */ -function createWrapper(props, fixture = manifestJson) { - return shallow( +/** + * create a simple wrapper for rendering our component + */ +function Subject({ fixture = manifestJson, ...props }) { + return ( <ThumbnailNavigation - canvasGroupings={ - new CanvasGroupings( - Utils.parseManifest(fixture).getSequences()[0].getCanvases(), - ).groupings() - } + canvasGroupings={new CanvasGroupings( + Utils.parseManifest(fixture).getSequences()[0].getCanvases(), + ).groupings()} canvasIndex={1} classes={{}} windowId="foobar" @@ -22,134 +23,114 @@ function createWrapper(props, fixture = manifestJson) { position="far-bottom" t={k => k} {...props} - />, + /> ); } +Subject.propTypes = { + fixture: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types +}; + +jest.mock( + 'react-virtualized-auto-sizer', + () => ({ children }) => children({ height: 600, width: 600 }), +); + describe('ThumbnailNavigation', () => { - let wrapper; - let rightWrapper; - beforeEach(() => { - wrapper = createWrapper(); - }); - it('renders the component', () => { - expect(wrapper.find('.mirador-thumb-navigation').length).toBe(1); + it('renders the component', async () => { + render(<Subject />); + + expect(screen.getByRole('grid')).toBeInTheDocument(); }); it('renders containers based off of number of canvases', () => { - expect(wrapper - .find('AutoSizer').dive().find('List').dive() - .find(ThumbnailCanvasGrouping).length).toEqual(3); + render(<Subject />); + + expect(screen.getAllByRole('gridcell').length).toEqual(3); }); - it('has a ref set used to reset on view change', () => { - expect(wrapper.instance().gridRef).not.toBe(null); + + // TODO: Test that we recalculate dimensions when the view changes (resetAfterIndex) + // TODO: Test that the window scrolls when the canvasIndex prop changes (scorllToItem) + + it('gives the grid a size', () => { + const { rerender } = render(<Subject />); + expect(screen.getByRole('grid')).toHaveStyle({ height: '150px', width: '100%' }); + + rerender(<Subject position="far-right" />); + expect(screen.getByRole('grid')).toHaveStyle({ height: '100%', minHeight: 0, width: '123px' }); }); - it('triggers a resetAfterIndex on view change', () => { - const mockReset = jest.fn(); - wrapper.instance().gridRef = { current: { resetAfterIndex: mockReset } }; - wrapper.setProps({ - canvasIndex: 1, - view: 'book', - }); - expect(mockReset).toHaveBeenCalled(); + + it('roughly doubles the width of the grid in book view', () => { + const { rerender } = render(<Subject position="far-right" />); + expect(screen.getByRole('grid')).toHaveStyle({ width: '123px' }); + + rerender(<Subject position="far-right" view="book" />); + expect(screen.getByRole('grid')).toHaveStyle({ width: '223px' }); }); - it('triggers a scrollToItem on canvasIndex change', () => { - const mockScroll = jest.fn(); - wrapper.instance().gridRef = { current: { scrollToItem: mockScroll } }; - wrapper.setProps({ - canvasIndex: 3, - }); - expect(mockScroll).toHaveBeenCalled(); + + it('calculates the scaled width of each cell', () => { + render(<Subject />); + + expect(screen.getAllByRole('gridcell')[0]).toHaveStyle({ width: '74px' }); }); - describe('calculating instance methods', () => { - beforeEach(() => { - rightWrapper = createWrapper({ - position: 'far-right', - }); - }); - it('style', () => { - expect(wrapper.instance().style()).toMatchObject({ height: '150px', width: '100%' }); - expect(rightWrapper.instance().style()).toMatchObject({ height: '100%', minHeight: 0, width: '123px' }); - }); - it('rightWidth', () => { - expect(wrapper.instance().rightWidth()).toEqual(100); - const mockReset = jest.fn(); - wrapper.instance().gridRef = { current: { resetAfterIndex: mockReset } }; - wrapper.setProps({ - canvasIndex: 1, - view: 'book', - }); - expect(wrapper.instance().rightWidth()).toEqual(200); - }); - it('item count is based off of number of canvases', () => { - expect(wrapper.instance().itemCount()).toEqual(3); - }); - it('calculateScaledSize', () => { - expect(wrapper.instance().calculateScaledSize(0)).toEqual(82); - expect(rightWrapper.instance().calculateScaledSize(0)).toEqual(158); - let zeroWidthWrapper = createWrapper({ position: 'far-right' }, zeroWidthFixture); - expect(zeroWidthWrapper.instance().calculateScaledSize(0)).toEqual(108); - zeroWidthWrapper = createWrapper({ position: 'bottom' }, zeroWidthFixture); - expect(zeroWidthWrapper.instance().calculateScaledSize(0)).toEqual(108); - }); - it('calculatingWidth', () => { - expect(wrapper.instance().calculatingWidth(1)).toEqual(100); - expect(wrapper.instance().calculatingWidth(2)).toEqual(200); - }); - it('areaHeight', () => { - expect(wrapper.instance().areaHeight()).toEqual(150); - expect(rightWrapper.instance().areaHeight(99)).toEqual(99); - }); - describe('without any canvases', () => { - it('returns the default for the calculated size', () => { - wrapper = createWrapper({ canvasGroupings: new CanvasGroupings([]).groupings() }); - expect(wrapper.instance().calculateScaledSize(0)).toEqual(108); - }); - }); + + it('calculates the scaled height of each cell when on the right', () => { + render(<Subject position="far-right" />); + expect(screen.getAllByRole('gridcell')[0]).toHaveStyle({ height: '150px' }); + }); + + it('keeps a minimum size for each cell', () => { + render(<Subject fixture={zeroWidthFixture} />); + + expect(screen.getAllByRole('gridcell')[0]).toHaveStyle({ width: '100px' }); + }); + + it('keeps a minimum size for each cell when on the right', () => { + render(<Subject fixture={zeroWidthFixture} position="far-right" />); + + expect(screen.getAllByRole('gridcell')[0]).toHaveStyle({ height: '100px' }); }); + describe('keyboard navigation', () => { const setNextCanvas = jest.fn(); const setPreviousCanvas = jest.fn(); - beforeEach(() => { - wrapper = createWrapper({ - canvasIndex: 1, - hasNextCanvas: true, - hasPreviousCanvas: true, - setNextCanvas, - setPreviousCanvas, - }); - }); + describe('handleKeyUp', () => { - it('handles right arrow by advancing the current canvas', () => { - wrapper.instance().handleKeyUp({ key: 'ArrowRight' }); + it('handles right arrow by advancing the current canvas', async () => { + render(<Subject canvasIndex={1} hasNextCanvas setNextCanvas={setNextCanvas} />); + + screen.getByRole('grid').focus(); + fireEvent.keyDown(screen.getByRole('grid'), { code: 'ArrowRight', key: 'ArrowRight' }); expect(setNextCanvas).toHaveBeenCalled(); }); it('handles down arrow by advancing the current canvas when the canvas is on the right', () => { - wrapper.setProps({ position: 'far-right' }); - wrapper.instance().handleKeyUp({ key: 'ArrowDown' }); + render(<Subject canvasIndex={1} hasNextCanvas position="far-right" setNextCanvas={setNextCanvas} />); + + screen.getByRole('grid').focus(); + fireEvent.keyDown(screen.getByRole('grid'), { code: 'ArrowDown', key: 'ArrowDown' }); expect(setNextCanvas).toHaveBeenCalled(); }); it('handles left arrow by selecting the previous canvas', () => { - wrapper.instance().handleKeyUp({ key: 'ArrowLeft' }); + render(<Subject canvasIndex={2} hasPreviousCanvas setPreviousCanvas={setPreviousCanvas} />); + + screen.getByRole('grid').focus(); + fireEvent.keyDown(screen.getByRole('grid'), { code: 'ArrowLeft', key: 'ArrowLeft' }); expect(setPreviousCanvas).toHaveBeenCalled(); }); it('handles up arrow by selecting the previous canvas when the canvas is on the right', () => { - wrapper.setProps({ position: 'far-right' }); - wrapper.instance().handleKeyUp({ key: 'ArrowUp' }); + render(<Subject canvasIndex={2} hasPreviousCanvas position="far-right" setPreviousCanvas={setPreviousCanvas} />); + + screen.getByRole('grid').focus(); + fireEvent.keyDown(screen.getByRole('grid'), { code: 'ArrowUp', key: 'ArrowUp' }); expect(setPreviousCanvas).toHaveBeenCalled(); }); }); }); - describe('when viewingDirection="right-to-left"', () => { - beforeEach(() => { - wrapper = createWrapper({ - viewingDirection: 'right-to-left', - }); - }); + describe('when viewingDirection="right-to-left"', () => { it('sets up react-window to be rtl', () => { - expect(wrapper - .find('AutoSizer').dive().find('List').dive() - .props().style.direction).toEqual('rtl'); + render(<Subject viewingDirection="right-to-left" />); + + expect(screen.getByRole('row').children[0]).toHaveStyle({ direction: 'rtl' }); // eslint-disable-line testing-library/no-node-access }); }); }); diff --git a/__tests__/src/components/VideoViewer.test.js b/__tests__/src/components/VideoViewer.test.js index 7b0281c803855cdb264ff034e3efe124809aaf3b..cc01105488a5ac41ea201fa3f21f7897c0366e88 100644 --- a/__tests__/src/components/VideoViewer.test.js +++ b/__tests__/src/components/VideoViewer.test.js @@ -1,6 +1,7 @@ import { shallow } from 'enzyme'; import { Utils } from 'manifesto.js'; import AnnotationFactory from '../../../src/lib/AnnotationFactory'; +import { render, screen } from 'test-utils'; import { VideoViewer } from '../../../src/components/VideoViewer'; import videoSimple from '../../fixtures/version-3/video.json'; import videoCaptions from '../../fixtures/version-3/video_captions.json'; @@ -9,60 +10,51 @@ import videoMultiCaptionsMultiAnno from '../../fixtures/version-3/video_captions /** create wrapper */ function createWrapper(props, suspenseFallback) { - return shallow( + return render( <VideoViewer classes={{}} - videoOptions={{ crossOrigin: 'anonymous' }} + videoOptions={{ crossOrigin: 'anonymous', 'data-testid': 'video' }} {...props} />, ); } describe('VideoViewer', () => { - let wrapper; describe('render', () => { - it('video', () => { - wrapper = createWrapper({ - canvas: Utils.parseManifest(videoSimple).getSequences()[0].getCanvases()[0], + it('videoResources', () => { + createWrapper({ + videoResources: [ + { getFormat: () => 'video/mp4', id: 1 }, + { getFormat: () => 'video/mp4', id: 2 }, + ], }, true); - expect(wrapper.exists('video[crossOrigin="anonymous"]')).toBe(true); // eslint-disable-line jsx-a11y/media-has-caption - expect(wrapper.contains(<source src="https://fixtures.iiif.io/video/indiana/30-minute-clock/medium/30-minute-clock.mp4" type="video/mp4" />)).toBe(true); + const video = screen.getByTestId('video'); + expect(video.querySelector('source:nth-of-type(1)')).toHaveAttribute('type', 'video/mp4'); // eslint-disable-line testing-library/no-node-access + expect(video.querySelector('source:nth-of-type(2)')).toHaveAttribute('type', 'video/mp4'); // eslint-disable-line testing-library/no-node-access }); - it('one caption', () => { - const canvas = Utils.parseManifest(videoCaptions).getSequences()[0].getCanvases()[0]; - /* cf selectors/annotations/getPresentAnnotationsCanvas */ - const annotations = canvas.__jsonld.annotations.flatMap((a) => AnnotationFactory.determineAnnotation(a)); - wrapper = createWrapper({ - annotations, - canvas, + it('passes through configurable options', () => { + createWrapper({ + videoResources: [ + { getFormat: () => 'video/mp4', id: 1 }, + ], }, true); - expect(wrapper.contains(<track src="https://fixtures.iiif.io/video/indiana/lunchroom_manners/lunchroom_manners.vtt" srcLang="en" />)).toBe(true); - expect(wrapper.exists('video[crossOrigin="anonymous"]')).toBe(true); // eslint-disable-line jsx-a11y/media-has-caption + expect(screen.getByTestId('video')).toHaveAttribute('crossOrigin', 'anonymous'); }); - it('multiples captions', () => { - const canvas = Utils.parseManifest(videoMultiCaptions).getSequences()[0].getCanvases()[0]; - /* cf selectors/annotations/getPresentAnnotationsCanvas */ - const annotations = canvas.__jsonld.annotations.flatMap((a) => AnnotationFactory.determineAnnotation(a)); - wrapper = createWrapper({ - annotations, - canvas, + it('captions', () => { + createWrapper({ + captions: [ + { getDefaultLabel: () => 'English', getProperty: () => 'en', id: 1 }, + { getDefaultLabel: () => 'French', getProperty: () => 'fr', id: 2 }, + ], + videoResources: [ + { getFormat: () => 'video/mp4', id: 1 }, + ], }, true); - expect(wrapper.contains(<track src="https://fixtures.iiif.io/video/indiana/lunchroom_manners/lunchroom_manners.vtt#en" srcLang="en" />)).toBe(true); - expect(wrapper.contains(<track src="https://fixtures.iiif.io/video/indiana/lunchroom_manners/lunchroom_manners.vtt#fr" srcLang="fr" />)).toBe(true); - expect(wrapper.exists('video[crossOrigin="anonymous"]')).toBe(true); // eslint-disable-line jsx-a11y/media-has-caption - }); - it('multiples captions in multiples annotations', () => { - const canvas = Utils.parseManifest(videoMultiCaptionsMultiAnno).getSequences()[0].getCanvases()[0]; - /* cf selectors/annotations/getPresentAnnotationsCanvas */ - const annotations = canvas.__jsonld.annotations.flatMap((a) => AnnotationFactory.determineAnnotation(a)); - wrapper = createWrapper({ - annotations, - canvas, - }, true); - expect(wrapper.contains(<track src="https://fixtures.iiif.io/video/indiana/lunchroom_manners/lunchroom_manners.vtt#en" srcLang="en" />)).toBe(true); - expect(wrapper.contains(<track src="https://fixtures.iiif.io/video/indiana/lunchroom_manners/lunchroom_manners.vtt#fr" srcLang="fr" />)).toBe(true); - expect(wrapper.contains(<track src="https://fixtures.iiif.io/video/indiana/lunchroom_manners/lunchroom_manners.vtt#ru" srcLang="ru" />)).toBe(true); - expect(wrapper.exists('video[crossOrigin="anonymous"]')).toBe(true); // eslint-disable-line jsx-a11y/media-has-caption + const video = screen.getByTestId('video'); + expect(video.querySelector('track:nth-of-type(1)')).toHaveAttribute('srcLang', 'en'); // eslint-disable-line testing-library/no-node-access + expect(video.querySelector('track:nth-of-type(1)')).toHaveAttribute('label', 'English'); // eslint-disable-line testing-library/no-node-access + expect(video.querySelector('track:nth-of-type(2)')).toHaveAttribute('srcLang', 'fr'); // eslint-disable-line testing-library/no-node-access + expect(video.querySelector('track:nth-of-type(2)')).toHaveAttribute('label', 'French'); // eslint-disable-line testing-library/no-node-access }); }); }); diff --git a/__tests__/src/components/ViewerInfo.test.js b/__tests__/src/components/ViewerInfo.test.js index 2700970c4cbf869f14b907dec106ec4e116a6af8..87adf53f13d2063696db585dab2968a899620c03 100644 --- a/__tests__/src/components/ViewerInfo.test.js +++ b/__tests__/src/components/ViewerInfo.test.js @@ -1,10 +1,9 @@ -import { shallow } from 'enzyme'; -import { Typography } from '@material-ui/core'; +import { render, screen } from 'test-utils'; import { ViewerInfo } from '../../../src/components/ViewerInfo'; /** create wrapper */ function createWrapper(props) { - return shallow( + return render( <ViewerInfo classes={{}} canvasCount={8} @@ -17,15 +16,9 @@ function createWrapper(props) { } describe('ViewerNavigation', () => { - let wrapper; - it('renders the component', () => { - wrapper = createWrapper(); - - expect(wrapper.find(Typography).length).toBe(2); - expect(wrapper.find(Typography).at(0) - .matchesElement(<Typography>pagination</Typography>)).toBe(true); - expect(wrapper.find(Typography).at(1) - .matchesElement(<Typography> • testLabel</Typography>)).toBe(true); + createWrapper(); + expect(screen.getByText('pagination', { selector: 'span' })).toBeInTheDocument(); + expect(screen.getByText(/testLabel/i, { selector: 'span' })).toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/ViewerNavigation.test.js b/__tests__/src/components/ViewerNavigation.test.js index 1c4efd3c456595f6659f3d813f2a3e6a52610b1c..7a70cd6e23174bafb33d921e4e87bce63f4f7566 100644 --- a/__tests__/src/components/ViewerNavigation.test.js +++ b/__tests__/src/components/ViewerNavigation.test.js @@ -1,11 +1,10 @@ -import { shallow } from 'enzyme'; -import NavigationIcon from '@material-ui/icons/PlayCircleOutlineSharp'; -import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { ViewerNavigation } from '../../../src/components/ViewerNavigation'; /** create wrapper */ function createWrapper(props) { - return shallow( + return render( <ViewerNavigation classes={{}} canvases={[1, 2]} @@ -16,125 +15,140 @@ function createWrapper(props) { } describe('ViewerNavigation', () => { - let wrapper; let setNextCanvas; let setPreviousCanvas; beforeEach(() => { setNextCanvas = jest.fn(); setPreviousCanvas = jest.fn(); - wrapper = createWrapper({ + }); + it('renders the component', () => { + createWrapper({ hasNextCanvas: true, hasPreviousCanvas: false, setNextCanvas, setPreviousCanvas, }); - }); - it('renders the component', () => { - expect(wrapper.find('.mirador-osd-navigation').length).toBe(1); + const buttons = screen.queryAllByRole('button'); + expect(buttons[0].closest('div')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access }); describe('when next canvases are present', () => { it('nextCanvas button is not disabled', () => { - expect(wrapper.find('.mirador-next-canvas-button').prop('aria-label')).toBe('nextCanvas'); - expect(wrapper.find('.mirador-next-canvas-button').prop('disabled')).toBe(false); + createWrapper({ + hasNextCanvas: true, + hasPreviousCanvas: false, + setNextCanvas, + setPreviousCanvas, + }); + expect(screen.getByRole('button', { name: 'nextCanvas' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'nextCanvas' })).toBeEnabled(); }); - it('setNextCanvas function is called after click', () => { - wrapper.find('.mirador-next-canvas-button').simulate('click'); + it('setNextCanvas function is called after click', async () => { + createWrapper({ + hasNextCanvas: true, + hasPreviousCanvas: false, + setNextCanvas, + setPreviousCanvas, + }); + const user = userEvent.setup(); + await user.click(screen.getByRole('button', { name: 'nextCanvas' })); expect(setNextCanvas).toHaveBeenCalled(); }); }); describe('when next canvases are not present', () => { - it('nextCanvas button is disabled', () => { - const endWrapper = createWrapper(); - expect(endWrapper.find('.mirador-next-canvas-button').prop('disabled')).toBe(true); - endWrapper.find('.mirador-next-canvas-button').simulate('click'); - expect(setNextCanvas).not.toHaveBeenCalled(); + it('nextCanvas button is disabled', async () => { + createWrapper({ + hasNextCanvas: false, + hasPreviousCanvas: true, + setNextCanvas, + setPreviousCanvas, + }); + expect(screen.getByRole('button', { name: 'nextCanvas' })).toBeDisabled(); }); }); describe('when previous canvases are present', () => { - beforeEach(() => { - wrapper = createWrapper({ + it('previousCanvas button is not disabled', () => { + createWrapper({ hasNextCanvas: false, hasPreviousCanvas: true, setNextCanvas, setPreviousCanvas, }); + expect(screen.getByRole('button', { name: 'previousCanvas' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'previousCanvas' })).toBeEnabled(); }); - it('previousCanvas button is not disabled', () => { - expect(wrapper.find('.mirador-previous-canvas-button').prop('aria-label')).toBe('previousCanvas'); - expect(wrapper.find('.mirador-previous-canvas-button').prop('disabled')).toBe(false); - }); - it('setPreviousCanvas function is called after click', () => { - wrapper.find('.mirador-previous-canvas-button').simulate('click'); + it('setPreviousCanvas function is called after click', async () => { + createWrapper({ + hasNextCanvas: false, + hasPreviousCanvas: true, + setNextCanvas, + setPreviousCanvas, + }); + const user = userEvent.setup(); + await user.click(screen.getByRole('button', { name: 'previousCanvas' })); expect(setPreviousCanvas).toHaveBeenCalled(); }); }); describe('when previous canvases are not present', () => { it('disabled on previousCanvas button', () => { - expect(wrapper.find('.mirador-previous-canvas-button').prop('disabled')).toBe(true); - }); - it('setCanvas function is not called after click, as its disabled', () => { - wrapper.find('.mirador-previous-canvas-button').simulate('click'); - expect(setPreviousCanvas).not.toHaveBeenCalled(); + createWrapper({ + hasNextCanvas: true, + hasPreviousCanvas: false, + setNextCanvas, + setPreviousCanvas, + }); + expect(screen.getByRole('button', { name: 'previousCanvas' })).toBeDisabled(); }); }); describe('when viewingDirection is right-to-left', () => { - beforeEach(() => { - wrapper = createWrapper({ + it('changes the arrow styles', () => { + createWrapper({ hasNextCanvas: true, hasPreviousCanvas: true, setNextCanvas, setPreviousCanvas, viewingDirection: 'right-to-left', }); - }); - - it('changes the arrow styles', () => { - const previous = wrapper.find(MiradorMenuButton).first().children(NavigationIcon).props(); - const next = wrapper.find(MiradorMenuButton).last().children(NavigationIcon).props(); - expect(previous.style).toEqual({}); - expect(next.style).toEqual({ transform: 'rotate(180deg)' }); + expect(screen.getByRole('button', { name: 'previousCanvas' }).querySelector('svg')).not.toHaveStyle('transform: rotate(180deg);'); // eslint-disable-line testing-library/no-node-access + expect(screen.getByRole('button', { name: 'nextCanvas' }).querySelector('svg')).toHaveStyle('transform: rotate(180deg);'); // eslint-disable-line testing-library/no-node-access }); it('sets the dir="rtl"', () => { - expect(wrapper.find('div').props().dir).toBe('rtl'); + createWrapper({ + hasNextCanvas: true, + hasPreviousCanvas: true, + setNextCanvas, + setPreviousCanvas, + viewingDirection: 'right-to-left', + }); + const buttons = screen.queryAllByRole('button'); + expect(buttons[0].closest('div')).toHaveAttribute('dir', 'rtl'); // eslint-disable-line testing-library/no-node-access }); }); describe('when viewingDirection is top-to-bottom', () => { - beforeEach(() => { - wrapper = createWrapper({ + it('changes the arrow styles', () => { + createWrapper({ hasNextCanvas: true, hasPreviousCanvas: true, setNextCanvas, setPreviousCanvas, viewingDirection: 'top-to-bottom', }); - }); - - it('changes the arrow styles', () => { - const previous = wrapper.find(MiradorMenuButton).first().children(NavigationIcon).props(); - const next = wrapper.find(MiradorMenuButton).last().children(NavigationIcon).props(); - expect(previous.style).toEqual({ transform: 'rotate(270deg)' }); - expect(next.style).toEqual({ transform: 'rotate(90deg)' }); + expect(screen.getByRole('button', { name: 'previousCanvas' }).querySelector('svg')).toHaveStyle('transform: rotate(270deg);'); // eslint-disable-line testing-library/no-node-access + expect(screen.getByRole('button', { name: 'nextCanvas' }).querySelector('svg')).toHaveStyle('transform: rotate(90deg);'); // eslint-disable-line testing-library/no-node-access }); }); - describe('when viewingDirection is bottom-to-top', () => { - beforeEach(() => { - wrapper = createWrapper({ + it('changes the arrow styles', () => { + createWrapper({ hasNextCanvas: true, hasPreviousCanvas: true, setNextCanvas, setPreviousCanvas, viewingDirection: 'bottom-to-top', }); - }); - - it('changes the arrow styles', () => { - const previous = wrapper.find(MiradorMenuButton).first().children(NavigationIcon).props(); - const next = wrapper.find(MiradorMenuButton).last().children(NavigationIcon).props(); - expect(previous.style).toEqual({ transform: 'rotate(90deg)' }); - expect(next.style).toEqual({ transform: 'rotate(270deg)' }); + expect(screen.getByRole('button', { name: 'previousCanvas' }).querySelector('svg')).toHaveStyle('transform: rotate(90deg);'); // eslint-disable-line testing-library/no-node-access + expect(screen.getByRole('button', { name: 'nextCanvas' }).querySelector('svg')).toHaveStyle('transform: rotate(270deg);'); // eslint-disable-line testing-library/no-node-access }); }); }); diff --git a/__tests__/src/components/Window.test.js b/__tests__/src/components/Window.test.js index cfb4920115984a00f4b8d6aa7d848b6ef203c1b9..de3bc26c5c8ed9de8d4260d897af520e987a6103 100644 --- a/__tests__/src/components/Window.test.js +++ b/__tests__/src/components/Window.test.js @@ -1,60 +1,101 @@ -import { shallow } from 'enzyme'; +import { MosaicWindowContext } from 'react-mosaic-component/lib/contextTypes'; +import { render, screen } from 'test-utils'; + import { Window } from '../../../src/components/Window'; -import WindowTopBar from '../../../src/containers/WindowTopBar'; -import PrimaryWindow from '../../../src/containers/PrimaryWindow'; -import IIIFAuthentication from '../../../src/containers/IIIFAuthentication'; -import ErrorContent from '../../../src/containers/ErrorContent'; /** create wrapper */ -function createWrapper(props, context) { - return shallow( +function createWrapper(props, state, renderOptions) { + return render( <Window - windowId="123" + windowId="xyz" manifestId="foo" classes={{}} t={k => k} {...props} />, - { context }, + { + preloadedState: { + windows: { + xyz: { + collectionDialogOn: false, + companionWindowIds: [], + }, + }, + }, + }, + { renderOptions }, ); } describe('Window', () => { - let wrapper; it('should render outer element', () => { - wrapper = createWrapper(); - expect(wrapper.find('.mirador-window')).toHaveLength(1); + createWrapper(); + expect(screen.getByLabelText('window')).toHaveClass('mirador-window'); }); it('should render <WindowTopBar>', () => { - wrapper = createWrapper(); - expect(wrapper.find(WindowTopBar)).toHaveLength(1); + createWrapper(); + expect(screen.getByRole('navigation', { accessibleName: 'windowNavigation' })).toBeInTheDocument(); }); it('should render <PrimaryWindow>', () => { - wrapper = createWrapper(); - expect(wrapper.find(PrimaryWindow)).toHaveLength(1); - }); - it('renders <WindowAuthenticationBar>', () => { - wrapper = createWrapper(); - expect(wrapper.find(IIIFAuthentication)).toHaveLength(1); + createWrapper(); + expect(document.querySelector('.mirador-primary-window')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access }); - it('renders manifest error', () => { - wrapper = createWrapper({ manifestError: 'Invalid JSON' }); - expect(wrapper.find(ErrorContent)).toHaveLength(1); - expect(wrapper.find(ErrorContent).prop('error')).toEqual({ stack: 'Invalid JSON' }); + // See ErrorContent.test.js for futher testing of this functionality + it('renders alert box when there is an error', async () => { + createWrapper({ manifestError: 'Invalid JSON' }); + expect(screen.getByRole('alert')).toBeInTheDocument(); }); - describe('when workspaceType is mosaic', () => { - xit('calls the context mosaicWindowActions connectDragSource method to make WindowTopBar draggable', () => { + it('calls the context mosaicWindowActions connectDragSource method to make WindowTopBar draggable', () => { const connectDragSource = jest.fn(component => component); - wrapper = createWrapper({ windowDraggable: true, workspaceType: 'mosaic' }, { mosaicWindowActions: { connectDragSource } }); - expect(wrapper.find(WindowTopBar)).toHaveLength(1); + render( + <MosaicWindowContext.Provider value={{ mosaicWindowActions: { connectDragSource } }}> + <Window + windowId="xyz" + manifestId="foo" + classes={{}} + t={k => k} + windowDraggable + workspaceType="mosaic" + /> + </MosaicWindowContext.Provider>, + { + preloadedState: { + windows: { + xyz: { + collectionDialogOn: false, + companionWindowIds: [], + }, + }, + }, + }, + ); expect(connectDragSource).toHaveBeenCalled(); }); - it('does not call the context mosaicWindowActions connectDragSource when the windowDraggable is set to false', () => { const connectDragSource = jest.fn(component => component); - wrapper = createWrapper({ windowDraggable: false, workspaceType: 'mosaic' }, { mosaicWindowActions: { connectDragSource } }); - expect(wrapper.find(WindowTopBar)).toHaveLength(1); + render( + <MosaicWindowContext.Provider value={{ mosaicWindowActions: { connectDragSource } }}> + <Window + windowId="xyz" + manifestId="foo" + classes={{}} + t={k => k} + windowDraggable={false} + workspaceType="mosaic" + /> + </MosaicWindowContext.Provider>, + { + preloadedState: { + windows: { + xyz: { + collectionDialogOn: false, + companionWindowIds: [], + }, + }, + }, + }, + ); expect(connectDragSource).not.toHaveBeenCalled(); }); }); diff --git a/__tests__/src/components/WindowAuthenticationBar.test.js b/__tests__/src/components/WindowAuthenticationBar.test.js index a75e46d4a5f796a6846069dc3534a8c653366fd7..c4368b81132bfd1c1202473d4fc296444eb73404 100644 --- a/__tests__/src/components/WindowAuthenticationBar.test.js +++ b/__tests__/src/components/WindowAuthenticationBar.test.js @@ -1,20 +1,17 @@ -import { shallow } from 'enzyme'; -import Button from '@material-ui/core/Button'; -import Collapse from '@material-ui/core/Collapse'; -import DialogActions from '@material-ui/core/DialogActions'; -import Typography from '@material-ui/core/Typography'; -import SanitizedHtml from '../../../src/containers/SanitizedHtml'; +import { render, screen, within } from 'test-utils'; +import userEvent from '@testing-library/user-event'; +import { config } from 'react-transition-group'; // eslint-disable-line import/no-extraneous-dependencies import { WindowAuthenticationBar } from '../../../src/components/WindowAuthenticationBar'; /** * Helper function to create a shallow wrapper around AuthenticationLogout */ function createWrapper(props) { - return shallow( + return render( <WindowAuthenticationBar classes={{}} hasLogoutService - confirmButton="Click here" + confirmButton="Login" label="Log in to see more" onConfirm={() => {}} status="ok" @@ -25,58 +22,66 @@ function createWrapper(props) { ); } +/* eslint-disable testing-library/no-node-access */ describe('AuthenticationControl', () => { + let user; + beforeEach(() => { + user = userEvent.setup(); + }); it('renders nothing if the user is logged in and there is no logout service', () => { - const wrapper = createWrapper({ hasLogoutService: false }); - expect(wrapper.isEmptyRender()).toBe(true); + createWrapper({ hasLogoutService: false }); + // no logout service renders a single empty div + expect(document.querySelectorAll('div')).toHaveLength(1); }); it('renders a non-collapsing version if there is no description', () => { - const wrapper = createWrapper({ description: undefined, header: undefined }); - expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('Log in to see more'); - expect(wrapper.find(Button).children().text()).toEqual('Click here'); + createWrapper({ description: undefined, header: undefined }); + expect(screen.getByText('Log in to see more', { selector: 'span' })).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveTextContent('Login'); }); - it('renders a collapsable version if there is a description', () => { - const onConfirm = jest.fn(); - const wrapper = createWrapper({ description: 'long description', header: 'header', onConfirm }); - expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('Log in to see more'); - expect(wrapper.find(Button).at(0).find('span').text()).toEqual('continue'); - // is expandable - expect(wrapper.find(Collapse).prop('in')).toEqual(false); - wrapper.find(Button).at(0).simulate('click'); - expect(wrapper.find(Collapse).prop('in')).toEqual(true); - - // has more information - expect(wrapper.find(Collapse).find(SanitizedHtml).at(0).props().htmlString).toEqual('header'); - expect(wrapper.find(Collapse).find(SanitizedHtml).at(1).props().htmlString).toEqual('long description'); + it('renders a collapsable version if there is a description', async () => { + createWrapper({ description: 'long description', header: 'Login to Example Institution' }); + const continueBtn = document.querySelectorAll('.MuiButtonBase-root')[0]; + const collapseEl = document.querySelector('.MuiCollapse-hidden'); - // is recollapsable - wrapper.find(DialogActions).find(Button).at(0).simulate('click'); - expect(wrapper.find(Collapse).prop('in')).toEqual(false); - wrapper.find(Button).at(0).simulate('click'); + // disable transition animations for easier testing of the Mui Collapse open/close state + config.disabled = true; + // initial collapsed state: Presence of continue button text. Hidden cancelBtn, loginBtn, and description + expect(screen.getByText('continue')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'cancel' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Login' })).not.toBeInTheDocument(); + expect(within(collapseEl).getByText('long description')).not.toBeVisible(); + // click to expand + await user.click(continueBtn); + // expanded state: Removal of continue button text from DOM. Visible cancelBtn, loginBtn, and description + expect(screen.queryByText('continue')).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'cancel' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Login' })).toBeVisible(); + expect(within(collapseEl).getByText('long description')).toBeVisible(); + expect(collapseEl).toHaveClass('MuiCollapse-entered'); - // starts the auth process - wrapper.find(DialogActions).find(Button).at(1).simulate('click'); - expect(onConfirm).toHaveBeenCalled(); + // click the cancel button to collapse + await user.click(screen.getByRole('button', { name: 'cancel' })); + // collapsed state: Presence of continue button text. Hidden cancelBtn, loginBtn, and description + expect(screen.getByText('continue')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'cancel' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Login' })).not.toBeInTheDocument(); + expect(within(collapseEl).getByText('long description')).not.toBeVisible(); + // re-enable transition animation + config.disabled = false; }); - it('triggers an action when the confirm button is clicked', () => { + it('triggers an action when the confirm button is clicked', async () => { const onConfirm = jest.fn(); - const wrapper = createWrapper({ - onConfirm, - }); - - wrapper.find(Button).simulate('click'); + createWrapper({ onConfirm }); + await user.click(screen.getByRole('button', { name: 'Login' })); expect(onConfirm).toHaveBeenCalled(); }); - it('allows plugins to opt out of HTML sanitization (say, for absolutely trusted sources)', () => { - const description = <em>long description</em>; - const wrapper = createWrapper({ description, header: 'header', ruleSet: false }); - expect(wrapper.find(SanitizedHtml).length).toEqual(0); - expect(wrapper.find(Typography).at(0).text()).toEqual('Log in to see more'); - expect(wrapper.find(Typography).at(2).text()).toEqual('header: long description'); - expect(wrapper.find(Typography).at(2).find('em').text()).toEqual('long description'); + it('allows plugins to opt out of HTML sanitization (say, for absolutely trusted sources)', async () => { + const description = <em>long html description</em>; + createWrapper({ description, header: 'header', ruleSet: false }); + await screen.findByText('long html description', { selector: 'em' }); }); }); diff --git a/__tests__/src/components/WindowCanvasNavigationControls.test.js b/__tests__/src/components/WindowCanvasNavigationControls.test.js index 16a7f1154e1dcc0dcdea2e44abeba53bc432acb3..de08147cc359a800fcaae3b5f26e54b996c67af1 100644 --- a/__tests__/src/components/WindowCanvasNavigationControls.test.js +++ b/__tests__/src/components/WindowCanvasNavigationControls.test.js @@ -1,56 +1,45 @@ -import { shallow } from 'enzyme'; -import Paper from '@material-ui/core/Paper'; -import Typography from '@material-ui/core/Typography'; +import { render, screen } from 'test-utils'; import { WindowCanvasNavigationControls } from '../../../src/components/WindowCanvasNavigationControls'; -import ViewerInfo from '../../../src/containers/ViewerInfo'; -import ViewerNavigation from '../../../src/containers/ViewerNavigation'; -import ZoomControls from '../../../src/containers/ZoomControls'; -import { PluginHook } from '../../../src/components/PluginHook'; -/** create wrapper */ -function createWrapper(props) { - return shallow( +/** + * create a simple wrapper for rendering our component + */ +function Subject({ ...props }) { + return ( <WindowCanvasNavigationControls canvases={[]} canvasLabel="label" size={{ width: 300 }} windowId="abc" - zoomToWorld={() => {}} + zoomToWorld={jest.fn()} {...props} - />, + /> ); } describe('WindowCanvasNavigationControls', () => { - let wrapper; - const zoomToWorld = jest.fn(); - - it('renders properly', () => { - wrapper = createWrapper({ zoomToWorld }); - expect(wrapper.matchesElement( - <Paper square> - <ZoomControls zoomToWorld={zoomToWorld} /> - <ViewerNavigation /> - <ViewerInfo /> - <PluginHook /> - </Paper>, - )).toBe(true); + it('renders properly', async () => { + const { container } = render(<Subject />); + expect(screen.getByLabelText('previousCanvas', { selector: 'button' })).toBeInTheDocument(); + expect(screen.getByLabelText('nextCanvas', { selector: 'button' })).toBeInTheDocument(); + expect(screen.getByText('pagination')).toBeInTheDocument(); + expect(container.firstChild).not.toHaveClass('mirador-canvas-nav-stacked'); // eslint-disable-line testing-library/no-node-access }); it('renders only a screen-reader accessibile version when visible=false', () => { - wrapper = createWrapper({ visible: false }); - expect(wrapper.matchesElement(<Typography variant="srOnly"><ViewerInfo /></Typography>)).toBe(true); + const { container } = render(<Subject visible={false} />); + expect(container.firstChild).toHaveStyle({ height: '1px', margin: '-1px', width: '1px' }); // eslint-disable-line testing-library/no-node-access }); - it('sets the proper class/ZoomControls prop dependent on the size/width prop', () => { - wrapper = createWrapper(); - - expect(wrapper.find('.mirador-canvas-nav-stacked').length).toEqual(0); - expect(wrapper.find(ZoomControls).props().displayDivider).toBe(true); - - wrapper.setProps({ size: { width: 200 } }); + it('stacks the nav controls on small width screens', () => { + const { container } = render(<Subject size={{ width: 252 }} />); + expect(container.firstChild).toHaveClass('mirador-canvas-nav-stacked'); // eslint-disable-line testing-library/no-node-access + }); - expect(wrapper.find('.mirador-canvas-nav-stacked').length).toEqual(1); - expect(wrapper.find(ZoomControls).props().displayDivider).toBe(false); + it('shows the zoom control component when specified', () => { + render(<Subject showZoomControls />); + expect(screen.getByRole('button', { name: 'zoomIn' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'zoomOut' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'zoomReset' })).toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/WindowList.test.js b/__tests__/src/components/WindowList.test.js index 35818ccb74b5bec94926d88e8ce53ebae6eb551b..dcf626f003c57b27c895fc9e5fb28db52dde2a1f 100644 --- a/__tests__/src/components/WindowList.test.js +++ b/__tests__/src/components/WindowList.test.js @@ -1,11 +1,8 @@ -import { shallow } from 'enzyme'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import ListItemText from '@material-ui/core/ListItemText'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { WindowList } from '../../../src/components/WindowList'; describe('WindowList', () => { - let wrapper; let handleClose; let focusWindow; let titles; @@ -14,28 +11,30 @@ describe('WindowList', () => { focusWindow = jest.fn(); titles = {}; - wrapper = shallow( + render(<div data-testid="container" />); + }); + + it('renders without an error', () => { + render( <WindowList - containerId="mirador" - anchorEl={{}} + anchorEl={screen.getByTestId('container')} + open titles={titles} windowIds={[]} handleClose={handleClose} focusWindow={focusWindow} />, ); - }); - it('renders without an error', () => { - expect(wrapper.find(Menu).length).toBe(1); + expect(screen.getByRole('menu')).toBeInTheDocument(); }); describe('with a window without a matching manifest', () => { beforeEach(() => { - wrapper = shallow( + render( <WindowList - containerId="mirador" - anchorEl={{}} + anchorEl={screen.getByTestId('container')} + open titles={titles} windowIds={['xyz']} handleClose={handleClose} @@ -44,14 +43,11 @@ describe('WindowList', () => { ); }); - it('renders without an error', () => { - expect(wrapper.find(MenuItem).length).toBe(1); - expect(wrapper.find(MenuItem).key()).toBe('xyz'); - expect( - wrapper.find(MenuItem) - .matchesElement(<MenuItem><ListItemText>untitled</ListItemText></MenuItem>), - ).toBe(true); - wrapper.find(MenuItem).simulate('click', {}); + it('renders without an error', async () => { + const user = userEvent.setup(); + expect(screen.getByRole('menuitem')).toBeInTheDocument(); + await user.click(screen.getByRole('menuitem', { name: 'untitled' })); + expect(handleClose).toBeCalled(); expect(focusWindow).toBeCalledWith('xyz', true); }); @@ -61,10 +57,10 @@ describe('WindowList', () => { beforeEach(() => { titles = { xyz: 'Some title' }; - wrapper = shallow( + render( <WindowList - containerId="mirador" - anchorEl={{}} + anchorEl={screen.getByTestId('container')} + open titles={titles} windowIds={['xyz']} handleClose={handleClose} @@ -74,30 +70,29 @@ describe('WindowList', () => { }); it('renders without an error', () => { - expect(wrapper.find(MenuItem).length).toBe(1); - expect(wrapper.find(MenuItem).key()).toBe('xyz'); - expect( - wrapper.find(MenuItem) - .matchesElement(<MenuItem><ListItemText>Some title</ListItemText></MenuItem>), - ).toBe(true); + expect(screen.getByRole('menuitem', { name: 'Some title' })).toBeInTheDocument(); }); }); - describe('focus2ndListIitem', () => { - const mockListItem = jest.fn(); - /** */ - const mockSingleItemMenu = { querySelectorAll: () => [{ focus: mockListItem }] }; - /** */ - const mockMultiItemMenu = { querySelectorAll: () => ['Header', { focus: mockListItem }] }; + describe('with a focused window', () => { + beforeEach(() => { + titles = { abc: 'Abc', xyz: 'Some title' }; - it('does not set focus if there is only one list item (the header)', () => { - WindowList.focus2ndListIitem(mockSingleItemMenu); - expect(mockListItem).not.toHaveBeenCalled(); + render( + <WindowList + anchorEl={screen.getByTestId('container')} + open + titles={titles} + windowIds={['abc', 'xyz']} + focusedWindowId="xyz" + handleClose={handleClose} + focusWindow={focusWindow} + />, + ); }); - it('sets focus on the 2nd list item', () => { - WindowList.focus2ndListIitem(mockMultiItemMenu); - expect(mockListItem).toHaveBeenCalled(); + it('puts focus on the currently focused window', () => { + expect(screen.getByRole('menuitem', { name: 'Some title' })).toHaveFocus(); }); }); }); diff --git a/__tests__/src/components/WindowListButton.test.js b/__tests__/src/components/WindowListButton.test.js index 0cba161d6e1bf6dbe9ba02c501e256e4b51327cc..d889f578fbeb27af65a17644e9ecc9d613a3bfa2 100644 --- a/__tests__/src/components/WindowListButton.test.js +++ b/__tests__/src/components/WindowListButton.test.js @@ -1,40 +1,44 @@ -import { shallow } from 'enzyme'; -import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; -import WindowList from '../../../src/containers/WindowList'; +import { render, screen, within } from 'test-utils'; +import userEvent from '@testing-library/user-event'; + import { WindowListButton } from '../../../src/components/WindowListButton'; /** * Helper function to create a shallow wrapper around WindowListButton */ function createWrapper(props) { - return shallow( + return render( <WindowListButton t={str => str} windowCount={3} {...props} />, + { preloadedState: { workspace: { windowIds: ['abc123'] } } }, ); } describe('WindowListButton', () => { - let wrapper; - - it('passes the windowCount as BadgeProps to MiradorMenuButton', () => { - wrapper = createWrapper(); + it('shows the window count as a badge on the button', () => { + createWrapper(); - expect(wrapper.find(MiradorMenuButton).props().BadgeProps.badgeContent).toEqual(3); + expect(within(screen.getByRole('button')).getByText('3', { container: '.MuiBadge-badge' })).toBeInTheDocument(); }); it('disabled the MiradorMenuButton if the disabled prop is true', () => { - wrapper = createWrapper({ disabled: true }); + createWrapper({ disabled: true }); - expect(wrapper.find(MiradorMenuButton).props().disabled).toBe(true); + expect(screen.getByRole('button')).toBeDisabled(); }); - it('toggles the WindowList comonent when clicking on the MiradorMenuButton', () => { - wrapper = createWrapper(); - expect(wrapper.find(WindowList).length).toBe(0); - wrapper.find(MiradorMenuButton).simulate('click', { currentTarget: 'blah' }); - expect(wrapper.find(WindowList).length).toBe(1); + it('toggles the WindowList comonent when clicking on the MiradorMenuButton', async () => { + const user = userEvent.setup(); + createWrapper(); + await user.click(screen.getByRole('button')); + + expect(screen.getByText('openWindows', { container: '.MuiListSubheader-root' })).toBeInTheDocument(); + + await user.click(screen.getByRole('menuitem', { name: 'untitled' })); + + expect(screen.queryByText('openWindows', { container: '.MuiListSubheader-root' })).not.toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/WindowSideBar.test.js b/__tests__/src/components/WindowSideBar.test.js index 95eb09ff445b00d90e8b70c97a0d517ec1698e0b..0fa75243cf9bb7be907346cdfc7bdd06db3ffd20 100644 --- a/__tests__/src/components/WindowSideBar.test.js +++ b/__tests__/src/components/WindowSideBar.test.js @@ -1,38 +1,44 @@ -import { shallow } from 'enzyme'; -import Drawer from '@material-ui/core/Drawer'; +import { render, screen } from 'test-utils'; import { WindowSideBar } from '../../../src/components/WindowSideBar'; /** create wrapper */ -function createWrapper(props) { - return shallow( +function createWrapper({ ...props }) { + return render( <WindowSideBar - classes={{}} t={k => k} - windowId="1" + windowId="xyz" {...props} />, + { + preloadedState: { + windows: { + xyz: { + companionWindowIds: [], + suggestedSearches: null, + }, + }, + }, + }, ); } -describe('WindowSideBar', () => { +describe('WindowSideBar when closed', () => { it('renders without an error', () => { - const wrapper = createWrapper(); - expect(wrapper.find(Drawer).length).toBe(1); - expect(wrapper.find(Drawer).prop('open')).toBe(false); + createWrapper({}); + expect(screen.queryByRole('navigation', { accessibleName: 'sidebarPanelsNavigation' })).not.toBeInTheDocument(); }); +}); +describe('WindowSideBar when open', () => { it('renders in an open state', () => { - const wrapper = createWrapper({ sideBarOpen: true }); - expect(wrapper.find(Drawer).length).toBe(1); - expect(wrapper.find(Drawer).prop('open')).toBe(true); + createWrapper({ sideBarOpen: true }); + expect(screen.getByRole('navigation', { accessibleName: 'sidebarPanelsNavigation' })).toBeInTheDocument(); }); - it('when ltr', () => { - const wrapper = createWrapper(); - expect(wrapper.find(Drawer).prop('anchor')).toBe('left'); + it('Renders drawer ltr by default', () => { + createWrapper({ sideBarOpen: true }); + expect(screen.queryByRole('navigation', { accessibleName: 'sidebarPanelsNavigation' })).toHaveClass('MuiDrawer-paperAnchorLeft'); // eslint-disable-line testing-library/no-node-access }); - it('when rtl', () => { - const wrapper = createWrapper({ - direction: 'rtl', - }); - expect(wrapper.find(Drawer).prop('anchor')).toBe('right'); + it('Renders drawer rtl when specified', () => { + createWrapper({ direction: 'rtl', sideBarOpen: true }); + expect(screen.queryByRole('navigation', { accessibleName: 'sidebarPanelsNavigation' })).toHaveClass('MuiDrawer-paperAnchorRight'); // eslint-disable-line testing-library/no-node-access }); }); diff --git a/__tests__/src/components/WindowSideBarAnnotationsPanel.test.js b/__tests__/src/components/WindowSideBarAnnotationsPanel.test.js index b07a4c4dac7a93b94a1b71cbbdafb30a9cd45db2..6faa978d6d9936b24cf4b4e95b6ba2d15d6fb882 100644 --- a/__tests__/src/components/WindowSideBarAnnotationsPanel.test.js +++ b/__tests__/src/components/WindowSideBarAnnotationsPanel.test.js @@ -1,47 +1,47 @@ -import { shallow } from 'enzyme'; -import Typography from '@material-ui/core/Typography'; +import { render, screen } from 'test-utils'; +import i18next from 'i18next'; + import CanvasAnnotations from '../../../src/containers/CanvasAnnotations'; import { WindowSideBarAnnotationsPanel } from '../../../src/components/WindowSideBarAnnotationsPanel'; /** */ -function createWrapper(props) { - return shallow( +function createWrapper(props, state) { + return render( <WindowSideBarAnnotationsPanel annotationCount={4} classes={{}} id="xyz" - t={(key, args) => ({ args, key })} + t={i18next.t} windowId="abc" {...props} />, + { preloadedState: { companionWindows: { xyz: { content: 'annotations' } }, windows: { abc: {} }, ...state } }, ); } describe('WindowSideBarAnnotationsPanel', () => { let wrapper; - it('has a header', () => { - wrapper = createWrapper(); + it('has a heading', () => { + createWrapper(); - expect( - wrapper.props().title.key, - ).toBe('annotations'); + expect(screen.getByRole('heading')).toHaveTextContent('Annotations'); }); it('has the AnnotationSettings component', () => { - const titleControls = createWrapper().prop('titleControls'); - expect(titleControls.type.displayName).toEqual('Connect(WithPlugins(AnnotationSettings))'); + createWrapper(); + + expect(screen.getByRole('button', { name: 'highlightAllAnnotations' })).toBeInTheDocument(); }); it('renders the annotationsCount', () => { - wrapper = createWrapper(); - const translatedCount = wrapper.find(Typography).props().children; + createWrapper(); - expect(translatedCount.key).toEqual('showingNumAnnotations'); - expect(translatedCount.args.number).toEqual(4); + expect(screen.getByText('Showing 4 annotations')).toHaveClass('MuiTypography-subtitle2'); }); - it('renders a CanvasAnnotations for every selected canvas', () => { + // TODO: Requires a lot of state setup... + xit('renders a CanvasAnnotations for every selected canvas', () => { wrapper = createWrapper({ canvasIds: ['abc', 'xyz'], }); diff --git a/__tests__/src/components/WindowSideBarButtons.test.js b/__tests__/src/components/WindowSideBarButtons.test.js index 7fb8a94f8d049a9260abf118fab5ce70b9f2e61f..e2b03bf31fa7a734b9f6e0a7c3975961dffaa139 100644 --- a/__tests__/src/components/WindowSideBarButtons.test.js +++ b/__tests__/src/components/WindowSideBarButtons.test.js @@ -1,14 +1,14 @@ -import { mount } from 'enzyme'; -import Badge from '@material-ui/core/Badge'; -import Tabs from '@material-ui/core/Tabs'; -import Tab from '@material-ui/core/Tab'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; +import i18next from 'i18next'; import { WindowSideBarButtons } from '../../../src/components/WindowSideBarButtons'; /** create wrapper */ function createWrapper(props) { - return mount( + return render( <WindowSideBarButtons addCompanionWindow={() => {}} + t={i18next.t} {...props} panels={{ annotations: true, @@ -21,24 +21,27 @@ function createWrapper(props) { />, ); } - -describe('WindowSideBarButtons (shallow)', () => { +/* eslint-disable testing-library/no-node-access */ +describe('WindowSideBarButtons', () => { const windowId = 'test123'; + let user; let wrapper; beforeEach(() => { - wrapper = createWrapper({ windowId }); + user = userEvent.setup(); }); it('renders without an error', () => { - expect(wrapper.find(Tabs).length).toBe(1); + wrapper = createWrapper({ windowId }); + expect(screen.getByRole('tablist')).toBeInTheDocument(); }); - it('triggers the addCompanionWindow prop on click', () => { + it('triggers the addCompanionWindow prop on click', async () => { const addCompanionWindow = jest.fn(); wrapper = createWrapper({ addCompanionWindow, windowId }); - wrapper.find(Tabs).props().onChange({ target: { removeAttribute: () => {}, setAttribute: () => {} } }, 'info'); + await user.click(screen.getByRole('tab', { name: 'Information' })); + expect(addCompanionWindow).toHaveBeenCalledTimes(1); expect(addCompanionWindow).toHaveBeenCalledWith('info'); }); @@ -46,33 +49,39 @@ describe('WindowSideBarButtons (shallow)', () => { it('has a badge indicating if the annotations panel has annotations', () => { let tab; wrapper = createWrapper({ hasAnnotations: true, windowId }); - tab = wrapper.find(Tab).find('[value="annotations"]'); - expect(tab.find(Badge).props().invisible).toBe(false); + + tab = screen.getByRole('tab', { name: 'Annotations' }); + expect(tab).toBeInTheDocument(); + expect(tab.querySelector('.MuiBadge-dot:not(.MuiBadge-invisible)')).toBeInTheDocument(); + + wrapper.unmount(); wrapper = createWrapper({ hasAnnotations: false, hasAnyAnnotations: true, windowId }); - tab = wrapper.find(Tab).find('[value="annotations"]'); - expect(tab.find(Badge).props().invisible).toBe(true); + tab = screen.getByRole('tab', { name: 'Annotations' }); + expect(tab.querySelector('.MuiBadge-dot.MuiBadge-invisible')).toBeInTheDocument(); }); it('hides the annotation panel if there are no annotations', () => { wrapper = createWrapper({ hasAnyAnnotations: false, windowId }); - expect(wrapper.find('WithStyles(Tab)[value="annotations"]').length).toEqual(0); + + expect(screen.queryByRole('tab', { name: 'Annotations' })).not.toBeInTheDocument(); }); it('can hide annotation panel when configured to do so', () => { wrapper = createWrapper({ hasAnnotations: true, panels: { annotations: false }, windowId }); - expect(wrapper.find('WithStyles(Tab)[value="annotations"]').length).toEqual(0); + + expect(screen.queryByRole('tab', { name: 'Annotations' })).not.toBeInTheDocument(); }); describe('search', () => { it('by default is off', () => { - expect(wrapper.find('WithStyles(Tab)[value="search"]').length).toEqual(0); + expect(screen.queryByRole('tab', { name: 'Search' })).not.toBeInTheDocument(); }); it('can be configured to be on', () => { wrapper = createWrapper({ hasSearchService: true, panels: { search: true }, windowId }); - expect(wrapper.find('WithStyles(ForwardRef(Tab))[value="search"]').length).toEqual(1); + expect(screen.getByRole('tab', { name: 'Search' })).toBeInTheDocument(); }); it('has a badge indicating if the search panel has active annotations', () => { @@ -85,8 +94,10 @@ describe('WindowSideBarButtons (shallow)', () => { }, windowId, }); - tab = wrapper.find(Tab).find('[value="search"]'); - expect(tab.find(Badge).props().invisible).toBe(false); + tab = screen.getByRole('tab', { name: 'Search' }); + expect(tab.querySelector('.MuiBadge-dot:not(.MuiBadge-invisible)')).toBeInTheDocument(); + + wrapper.unmount(); wrapper = createWrapper({ hasSearchResults: false, @@ -96,20 +107,20 @@ describe('WindowSideBarButtons (shallow)', () => { }, windowId, }); - tab = wrapper.find(Tab).find('[value="search"]'); + tab = screen.getByRole('tab', { name: 'Search' }); - expect(tab.find(Badge).props().invisible).toBe(true); + expect(tab.querySelector('.MuiBadge-dot.MuiBadge-invisible')).toBeInTheDocument(); }); }); describe('layers', () => { it('by default is off', () => { - expect(wrapper.find('WithStyles(Tab)[value="layers"]').length).toEqual(0); + expect(screen.queryByRole('tab', { name: 'Layers' })).not.toBeInTheDocument(); }); it('can be configured to be on', () => { wrapper = createWrapper({ hasAnyLayers: true, panels: { layers: true }, windowId }); - expect(wrapper.find('WithStyles(ForwardRef(Tab))[value="layers"]').length).toEqual(1); + expect(screen.getByRole('tab', { name: 'Layers' })).toBeInTheDocument(); }); it('has a badge indicating if there are currently any layers', () => { @@ -122,8 +133,10 @@ describe('WindowSideBarButtons (shallow)', () => { }, windowId, }); - tab = wrapper.find(Tab).find('[value="layers"]'); - expect(tab.find(Badge).props().invisible).toBe(false); + tab = screen.getByRole('tab', { name: 'Layers' }); + expect(tab.querySelector('.MuiBadge-dot:not(.MuiBadge-invisible)')).toBeInTheDocument(); + + wrapper.unmount(); wrapper = createWrapper({ hasAnyLayers: true, @@ -133,9 +146,9 @@ describe('WindowSideBarButtons (shallow)', () => { }, windowId, }); - tab = wrapper.find(Tab).find('[value="layers"]'); + tab = screen.getByRole('tab', { name: 'Layers' }); - expect(tab.find(Badge).props().invisible).toBe(true); + expect(tab.querySelector('.MuiBadge-dot.MuiBadge-invisible')).toBeInTheDocument(); }); }); }); diff --git a/__tests__/src/components/WindowSideBarCanvasPanel.test.js b/__tests__/src/components/WindowSideBarCanvasPanel.test.js index 3a9951f7377f146c926c4ed4c56b61c70a477d4a..9c3c48ca0ac8ebef6279fba2c6ba317018967054 100644 --- a/__tests__/src/components/WindowSideBarCanvasPanel.test.js +++ b/__tests__/src/components/WindowSideBarCanvasPanel.test.js @@ -1,9 +1,8 @@ -import { shallow } from 'enzyme'; +import { render, screen, within } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { Utils } from 'manifesto.js'; -import compact from 'lodash/compact'; + import { WindowSideBarCanvasPanel } from '../../../src/components/WindowSideBarCanvasPanel'; -import SidebarIndexList from '../../../src/containers/SidebarIndexList'; -import CompanionWindow from '../../../src/containers/CompanionWindow'; import manifestJson from '../../fixtures/version-2/019.json'; /** @@ -20,7 +19,7 @@ function createWrapper(props) { sequences = Utils.parseManifest(manifestJson).getSequences(); } - return shallow( + return render( <WindowSideBarCanvasPanel id="asdf" canvases={canvases} @@ -32,47 +31,57 @@ function createWrapper(props) { updateVariant={() => {}} selectedCanvases={[canvases[1]]} sequences={sequences} + sequenceId={sequences[0].id} variant="item" {...props} />, + { preloadedState: { companionWindows: { asdf: { content: 'canvases' } } } }, ); } describe('WindowSideBarCanvasPanel', () => { it('renders SidebarIndexList', () => { - const wrapper = createWrapper({ multipleSequences: false }); - expect(wrapper.find(CompanionWindow).props().title).toBe('canvasIndex'); - expect(wrapper.find(SidebarIndexList).length).toBe(1); + createWrapper({ multipleSequences: false }); + + expect(screen.getByRole('heading', { name: 'canvasIndex' })).toBeInTheDocument(); + expect(screen.getByRole('menu')).toBeInTheDocument(); }); it('without a treeStructure will not render the table of contents tab', () => { - const wrapper = createWrapper({ multipleSequences: false }); - expect( - compact(wrapper.find(CompanionWindow).props().titleControls.props.children) - .length, - ).toBe(2); + createWrapper({ multipleSequences: false }); + + expect(screen.queryByRole('tab', { name: 'tableOfContentsList' })).not.toBeInTheDocument(); }); it('renders form control when multiple sequences present', () => { - const wrapper = createWrapper({ multipleSequences: true }); + createWrapper({ multipleSequences: true, showToc: true }); - expect(wrapper.find(CompanionWindow).props().titleControls.props.children[0] - .type.displayName.includes('FormControl')).toBe(true); + expect(screen.getByRole('tab', { name: 'tableOfContentsList' })).toBeInTheDocument(); }); - it('renders correct number of sequences in form control', () => { - const wrapper = createWrapper({ multipleSequences: true }); + it('renders correct number of sequences in form control', async () => { + const user = userEvent.setup(); + const updateSequence = jest.fn(); + createWrapper({ multipleSequences: true, updateSequence }); - expect(wrapper.find(CompanionWindow).props().titleControls.props.children[0] - .props.children.props.children.length).toBe(2); + expect(screen.getByTestId('sequence-select')).toHaveTextContent('a'); + await user.click(within(screen.getByTestId('sequence-select')).getByRole('combobox')); + + const listbox = within(screen.getByRole('listbox')); + expect(listbox.getAllByRole('option')).toHaveLength(2); + + await user.click(listbox.getByRole('option', { name: 'b' })); + expect(updateSequence).toHaveBeenCalledWith('b'); }); describe('handleVariantChange', () => { - it('updates the variant', () => { + it('updates the variant', async () => { + const user = userEvent.setup(); const updateVariant = jest.fn(); - const wrapper = createWrapper({ multipleSequences: false, updateVariant }); - wrapper.instance().handleVariantChange({}, 'item'); - expect(updateVariant).toHaveBeenCalledWith('item'); + createWrapper({ multipleSequences: false, updateVariant }); + + await user.click(screen.getByRole('tab', { name: 'thumbnailList' })); + expect(updateVariant).toHaveBeenCalledWith('thumbnail'); }); }); }); diff --git a/__tests__/src/components/WindowSideBarInfoPanel.test.js b/__tests__/src/components/WindowSideBarInfoPanel.test.js index d830bd1a3d3080cc56fd68da551e86c5a830eaaa..7709eb43fa6cc9a64cf4c227371f1e829213e4fe 100644 --- a/__tests__/src/components/WindowSideBarInfoPanel.test.js +++ b/__tests__/src/components/WindowSideBarInfoPanel.test.js @@ -1,50 +1,37 @@ -import { shallow } from 'enzyme'; +import { render, screen } from 'test-utils'; + import { WindowSideBarInfoPanel } from '../../../src/components/WindowSideBarInfoPanel'; -import CanvasInfo from '../../../src/containers/CanvasInfo'; -import ManifestInfo from '../../../src/containers/ManifestInfo'; -import ManifestRelatedLinks from '../../../src/containers/ManifestRelatedLinks'; /** create wrapper */ function createWrapper(props) { - return shallow( + return render( <WindowSideBarInfoPanel id="asdf" windowId="zxcv" t={str => str} {...props} />, + { preloadedState: { companionWindows: { asdf: { content: 'info' } } } }, ); } describe('WindowSideBarInfoPanel', () => { - let wrapper; - describe('when metadata is present', () => { it('renders headers', () => { - wrapper = createWrapper(); - expect( - wrapper.props().title, - ).toBe('aboutThisItem'); + createWrapper(); + expect(screen.getByRole('heading', { name: 'aboutThisItem' })).toBeInTheDocument(); }); it('renders the manifest elements', () => { - wrapper = createWrapper(); - expect(wrapper.find(ManifestInfo).length).toBe(1); - expect(wrapper.find(ManifestRelatedLinks).length).toBe(1); + createWrapper(); + + expect(screen.getByRole('heading', { name: 'resource' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'related' })).toBeInTheDocument(); }); it('renders the canvas elements', () => { - wrapper = createWrapper({ canvasIds: ['1', '2'] }); - expect(wrapper.find(CanvasInfo).length).toBe(2); - let canvasInfo = wrapper.find(CanvasInfo).at(0); - - expect(canvasInfo.props().canvasId).toEqual('1'); - expect(canvasInfo.props().index).toEqual(0); - expect(canvasInfo.props().totalSize).toEqual(2); + createWrapper({ canvasIds: ['1', '2'] }); - canvasInfo = wrapper.find(CanvasInfo).at(1); - expect(canvasInfo.props().canvasId).toEqual('2'); - expect(canvasInfo.props().index).toEqual(1); - expect(canvasInfo.props().totalSize).toEqual(2); + expect(screen.getAllByRole('heading', { name: 'currentItem' }).length).toEqual(2); }); }); }); diff --git a/__tests__/src/components/WindowThumbnailSettings.test.js b/__tests__/src/components/WindowThumbnailSettings.test.js index 4d16056404387c7bf1b5391bb89485335bdaed97..27c0c034355c321aa1e5403432d682098e8d4589 100644 --- a/__tests__/src/components/WindowThumbnailSettings.test.js +++ b/__tests__/src/components/WindowThumbnailSettings.test.js @@ -1,12 +1,10 @@ -import { shallow } from 'enzyme'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import ListSubheader from '@material-ui/core/ListSubheader'; -import MenuItem from '@material-ui/core/MenuItem'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { WindowThumbnailSettings } from '../../../src/components/WindowThumbnailSettings'; /** create wrapper */ function createWrapper(props) { - return shallow( + return render( <WindowThumbnailSettings classes={{}} direction="ltr" @@ -20,36 +18,45 @@ function createWrapper(props) { describe('WindowThumbnailSettings', () => { it('renders all elements correctly', () => { - const wrapper = createWrapper(); - expect(wrapper.find(ListSubheader).length).toBe(1); - const labels = wrapper.find(FormControlLabel); - expect(labels.length).toBe(3); - expect(labels.at(0).props().value).toBe('off'); - expect(labels.at(1).props().value).toBe('far-bottom'); - expect(labels.at(2).props().value).toBe('far-right'); + createWrapper(); + expect(screen.getByRole('presentation', { selector: 'li' })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: /off/ })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: /bottom/ })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: /right/ })).toBeInTheDocument(); }); - - it('should set the correct label active (by setting the secondary color)', () => { - let wrapper = createWrapper({ thumbnailNavigationPosition: 'far-bottom' }); - expect(wrapper.find(FormControlLabel).at(1).props().control.props.color).toEqual('secondary'); - expect(wrapper.find(FormControlLabel).at(2).props().control.props.color).not.toEqual('secondary'); - - wrapper = createWrapper({ thumbnailNavigationPosition: 'far-right' }); - expect(wrapper.find(FormControlLabel).at(2).props().control.props.color).toEqual('secondary'); + it('for far-bottom it should set the correct label active (by setting the secondary color)', () => { + createWrapper({ thumbnailNavigationPosition: 'far-bottom' }); + expect(screen.getByRole('menuitem', { name: /bottom/ }).querySelector('svg')).toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access + expect(screen.getByRole('menuitem', { name: /right/ }).querySelector('svg')).not.toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access + expect(screen.getByRole('menuitem', { name: /off/ }).querySelector('svg')).not.toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access + }); + it('for far-right it should set the correct label active (by setting the secondary color)', () => { + createWrapper({ thumbnailNavigationPosition: 'far-right' }); + expect(screen.getByRole('menuitem', { name: /right/ }).querySelector('svg')).toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access + expect(screen.getByRole('menuitem', { name: /off/ }).querySelector('svg')).not.toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access + expect(screen.getByRole('menuitem', { name: /bottom/ }).querySelector('svg')).not.toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access }); - it('updates state when the thumbnail config selection changes', () => { + it('updates state when the thumbnail config selection changes', async () => { const setWindowThumbnailPosition = jest.fn(); - const wrapper = createWrapper({ setWindowThumbnailPosition }); + const user = userEvent.setup(); + createWrapper({ setWindowThumbnailPosition }); + const menuItems = screen.queryAllByRole('menuitem'); + expect(menuItems.length).toBe(3); + expect(menuItems[0]).toBeInTheDocument(); + expect(menuItems[1]).toBeInTheDocument(); + expect(menuItems[2]).toBeInTheDocument(); - wrapper.find(MenuItem).at(0).simulate('click'); + await user.click(menuItems[0]); expect(setWindowThumbnailPosition).toHaveBeenCalledWith('xyz', 'off'); - wrapper.find(MenuItem).at(2).simulate('click'); + await user.click(menuItems[1]); + expect(setWindowThumbnailPosition).toHaveBeenCalledWith('xyz', 'far-bottom'); + await user.click(menuItems[2]); expect(setWindowThumbnailPosition).toHaveBeenCalledWith('xyz', 'far-right'); }); it('when rtl flips an icon', () => { - const wrapper = createWrapper({ direction: 'rtl' }); - expect(wrapper.find(FormControlLabel).at(2).props().control.props.style).toEqual({ transform: 'rotate(180deg)' }); + createWrapper({ direction: 'rtl' }); + expect(screen.getByRole('menuitem', { name: /right/ }).querySelector('svg')).toHaveStyle('transform: rotate(180deg);'); // eslint-disable-line testing-library/no-node-access }); }); diff --git a/__tests__/src/components/WindowTopBar.test.js b/__tests__/src/components/WindowTopBar.test.js index 69bf35bf27798e252d9605187fb284055518eda0..8f2984dc26652be668d659a7180263f93725300f 100644 --- a/__tests__/src/components/WindowTopBar.test.js +++ b/__tests__/src/components/WindowTopBar.test.js @@ -1,90 +1,108 @@ -import { shallow } from 'enzyme'; - -import Toolbar from '@material-ui/core/Toolbar'; -import AppBar from '@material-ui/core/AppBar'; - -import WindowTopMenuButton from '../../../src/containers/WindowTopMenuButton'; -import WindowTopBarPluginArea from '../../../src/containers/WindowTopBarPluginArea'; -import WindowTopBarPluginMenu from '../../../src/containers/WindowTopBarPluginMenu'; -import WindowTopBarTitle from '../../../src/containers/WindowTopBarTitle'; -import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; -import FullScreenButton from '../../../src/containers/FullScreenButton'; +import { screen, fireEvent, render } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { WindowTopBar } from '../../../src/components/WindowTopBar'; +import FullscreenContext from '../../../src/contexts/FullScreenContext'; + /** create wrapper */ -function createWrapper(props) { - return shallow( - <WindowTopBar - windowId="xyz" - classes={{}} - t={str => str} - maximizeWindow={() => {}} - maximized={false} - minimizeWindow={() => {}} - removeWindow={() => {}} - toggleWindowSideBar={() => {}} - {...props} - />, +function Subject({ ...props }) { + return ( + <FullscreenContext.Provider value={jest.fn()}> + <WindowTopBar + windowId="xyz" + classes={{}} + t={str => str} + focusWindow={() => {}} + maximized={false} + maximizeWindow={() => {}} + minimizeWindow={() => {}} + removeWindow={() => {}} + {...props} + /> + </FullscreenContext.Provider> ); } describe('WindowTopBar', () => { - it('renders all default components', () => { - const wrapper = createWrapper(); - expect(wrapper.find(AppBar).length).toBe(1); - expect(wrapper.find(Toolbar).length).toBe(1); - expect(wrapper.find(MiradorMenuButton).length).toBe(3); - expect(wrapper.find(WindowTopBarTitle).length).toBe(1); - expect(wrapper.find(WindowTopBarPluginArea).length).toBe(1); - expect(wrapper.find(WindowTopBarPluginMenu).length).toBe(1); - expect(wrapper.find(WindowTopMenuButton).length).toBe(1); - expect(wrapper.find(FullScreenButton).length).toBe(0); + let user; + beforeEach(() => { + user = userEvent.setup(); }); - it('triggers window focus when clicked', () => { - const focusWindow = jest.fn(); - const wrapper = createWrapper({ focusWindow }); - wrapper.find(Toolbar).simulate('mouseDown'); - expect(focusWindow).toHaveBeenCalled(); + it('renders all default components', () => { + render(<Subject />); + expect(screen.getByRole('navigation', { name: 'windowNavigation' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'toggleWindowSideBar' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'windowMenu' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'maximizeWindow' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'closeWindow' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'allowFullscreen' })).not.toBeInTheDocument(); }); - it('passes correct props to <IconButton/>', () => { - const toggleWindowSideBar = jest.fn(); - const wrapper = createWrapper({ toggleWindowSideBar }); - expect(wrapper.find(MiradorMenuButton).first().props().onClick).toBe(toggleWindowSideBar); + it('uses allow flags to override defaults', () => { + render(<Subject + allowWindowSideBar={false} + allowClose={false} + allowMaximize={false} + allowTopMenuButton={false} + allowFullscreen + />); + expect(screen.queryByRole('button', { name: 'toggleWindowSideBar' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'windowMenu' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'maximizeWindow' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'closeWindow' })).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'workspaceFullScreen' })).toBeInTheDocument(); }); - it('passes correct props to <WindowTopBarButtons/>', () => { - const wrapper = createWrapper(); - expect(wrapper.find(WindowTopBarPluginMenu).first().props().windowId).toBe('xyz'); + it('triggers window focus when clicked', () => { + const focusWindow = jest.fn(); + render(<Subject focusWindow={focusWindow} />); + const toolbar = screen.getByRole('navigation', { name: 'windowNavigation' }).firstChild; // eslint-disable-line testing-library/no-node-access + expect(toolbar).toBeInTheDocument(); + // we specifically need mouseDown not click for MUI Toolbar here + fireEvent.mouseDown(toolbar); + expect(focusWindow).toHaveBeenCalledTimes(1); }); - it('passe correct props to <WindowTopMenuButton', () => { - const wrapper = createWrapper(); - expect(wrapper.find(WindowTopMenuButton).first().props().windowId).toBe('xyz'); + it('passes correct callback to toggleWindowSideBar button', async () => { + const toggleWindowSideBar = jest.fn(); + render( + <Subject allowWindowSideBar toggleWindowSideBar={toggleWindowSideBar} />, + { preloadedState: { windows: { xyz: { sideBarOpen: false } } } }, + ); + const button = screen.getByRole('button', { name: 'toggleWindowSideBar' }); + expect(button).toBeInTheDocument(); + await user.click(button); + expect(toggleWindowSideBar).toHaveBeenCalledTimes(1); }); - it('passes correct props to <Button/>', () => { + it('passes correct callback to closeWindow button', async () => { const removeWindow = jest.fn(); - const wrapper = createWrapper({ removeWindow }); - expect(wrapper.find(MiradorMenuButton).last().props().onClick).toBe(removeWindow); + render(<Subject allowClose removeWindow={removeWindow} />); + const button = screen.getByRole('button', { name: 'closeWindow' }); + expect(button).toBeInTheDocument(); + await user.click(button); + expect(removeWindow).toHaveBeenCalledTimes(1); }); - it('passes correct props to <Button/>', () => { + it('passes correct callback to maximizeWindow button', async () => { const maximizeWindow = jest.fn(); - const wrapper = createWrapper({ maximizeWindow }); - expect(wrapper.find(MiradorMenuButton).at(1).props().onClick).toBe(maximizeWindow); + render(<Subject allowMaximize maximizeWindow={maximizeWindow} />); + const button = screen.getByRole('button', { name: 'maximizeWindow' }); + expect(button).toBeInTheDocument(); + await user.click(button); + expect(maximizeWindow).toHaveBeenCalledTimes(1); }); it('close button is configurable', () => { - expect(createWrapper({ allowClose: false }).find('.mirador-window-close').length).toEqual(0); + render(<Subject allowClose={false} />); + const button = screen.queryByRole('button', { name: 'closeWindow' }); + expect(button).not.toBeInTheDocument(); }); it('maximize button is configurable', () => { - expect(createWrapper({ allowMaximize: false }).find('.mirador-window-maximize').length).toEqual(0); - }); - - it('fullscreen button is configurable', () => { - expect(createWrapper({ allowFullscreen: true }).find(FullScreenButton).length).toEqual(1); + render(<Subject allowMaximize={false} />); + const button = screen.queryByRole('button', { name: 'maximizeWindow' }); + expect(button).not.toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/WindowTopBarPluginArea.test.js b/__tests__/src/components/WindowTopBarPluginArea.test.js index 1b9feb8be0def3ba4fe84d3c3676be5642a0f8ee..747edf553e1e078c9e8d4ef17a847a8cc691550e 100644 --- a/__tests__/src/components/WindowTopBarPluginArea.test.js +++ b/__tests__/src/components/WindowTopBarPluginArea.test.js @@ -1,8 +1,14 @@ -import { shallow } from 'enzyme'; +import { render, screen } from 'test-utils'; import { WindowTopBarPluginArea } from '../../../src/components/WindowTopBarPluginArea'; -import { PluginHook } from '../../../src/components/PluginHook'; -it('renders the component', () => { - const wrapper = shallow(<WindowTopBarPluginArea />); - expect(wrapper.find(PluginHook).length).toBe(1); +/** */ +const mockComponent = () => ( + <div data-testid="test" /> +); + +describe('WindowTopBarPluginArea', () => { + it('renders the component', () => { + render(<WindowTopBarPluginArea PluginComponents={[mockComponent]} />); + expect(screen.getByTestId('test')).toBeInTheDocument(); + }); }); diff --git a/__tests__/src/components/WindowTopBarPluginMenu.test.js b/__tests__/src/components/WindowTopBarPluginMenu.test.js index f41e301d434eed9655bb116dda20269dc4a7ff22..2d887db3243d0f6ad0acbbaeda42573f3df13ee1 100644 --- a/__tests__/src/components/WindowTopBarPluginMenu.test.js +++ b/__tests__/src/components/WindowTopBarPluginMenu.test.js @@ -1,70 +1,60 @@ -import { shallow } from 'enzyme'; -import Menu from '@material-ui/core/Menu'; -import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; -import { PluginHook } from '../../../src/components/PluginHook'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + import { WindowTopBarPluginMenu } from '../../../src/components/WindowTopBarPluginMenu'; /** create wrapper */ -function createWrapper(props) { - return shallow( +function Subject({ ...props }) { + return ( <WindowTopBarPluginMenu - containerId="abc123-container" t={k => k} windowId="abc123" {...props} - />, + /> ); } +// needs to be a non-functional component to accept forwardRef the way we have it set up +/** */ +class mockComponentA extends React.Component { + /** */ + render() { + return ( + <div data-testid="testA" /> + ); + } +} describe('WindowTopBarPluginMenu', () => { - let wrapper; - describe('when there are no plugins present', () => { it('renders nothing (and no Button/Menu/PluginHook)', () => { - wrapper = createWrapper(); - expect(wrapper.isEmptyRender()).toBe(true); + render(<Subject />); + expect(screen.queryByTestId('testA')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'windowPluginMenu' })).not.toBeInTheDocument(); }); }); describe('when there are plugins present', () => { - const PluginComponents = ['Plugin1', 'Plugin2']; - - it('renders the Button, Menu, and PluginHook', () => { - wrapper = createWrapper({ PluginComponents }); - - expect(wrapper.find(Menu).length).toBe(1); - expect(wrapper.find(MiradorMenuButton).length).toBe(1); - expect(wrapper.find(PluginHook).length).toBe(1); + let user; + beforeEach(() => { + user = userEvent.setup(); + render(<Subject PluginComponents={[mockComponentA]} />); }); - it('the Menu is controlled by the Button clicks/local state', () => { - wrapper = createWrapper({ PluginComponents }); - - expect(wrapper.find(Menu).props().open).toBe(false); - expect(wrapper.state().anchorEl).toBeNull(); - - wrapper.find(MiradorMenuButton).simulate('click', { currentTarget: 'Button' }); - expect(wrapper.find(Menu).props().open).toBe(true); - expect(wrapper.state().anchorEl).toEqual('Button'); + it('renders the Button', async () => { + expect(screen.getByRole('button', { name: 'windowPluginMenu' })).toBeInTheDocument(); }); - it('the onClose prop of the Menu updates the open prop/state', () => { - wrapper = createWrapper({ PluginComponents }); - wrapper.setState({ anchorEl: 'Button' }); - expect(wrapper.find(Menu).props().open).toBe(true); - - wrapper.find(Menu).props().onClose(); - expect(wrapper.find(Menu).props().open).toBe(false); - expect(wrapper.state().anchorEl).toBeNull(); - }); + it('the Menu is controlled by the Button clicks', async () => { + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + expect(screen.queryByTestId('testA')).not.toBeInTheDocument(); - it('explicitly passes the local close handler to the PluginHook', () => { - wrapper = createWrapper({ PluginComponents }); + // open + await user.click(screen.getByRole('button', { name: 'windowPluginMenu' })); + expect(screen.getByRole('menu')).toBeInTheDocument(); - wrapper.setState({ anchorEl: 'Button' }); - expect(wrapper.state().anchorEl).toEqual('Button'); - expect(wrapper.find(PluginHook).props().handleClose()); - expect(wrapper.state().anchorEl).toBeNull(); + await user.keyboard('{Escape}'); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); }); }); }); diff --git a/__tests__/src/components/WindowTopBarTitle.test.js b/__tests__/src/components/WindowTopBarTitle.test.js index 282b31673aef36fd1bbacd7e01fb0912175f1093..743069fc11b335b86337b5f5e63838916c61f0d5 100644 --- a/__tests__/src/components/WindowTopBarTitle.test.js +++ b/__tests__/src/components/WindowTopBarTitle.test.js @@ -1,11 +1,10 @@ -import { shallow } from 'enzyme'; -import Skeleton from '@material-ui/lab/Skeleton'; +import { render, screen } from 'test-utils'; import { WindowTopBarTitle } from '../../../src/components/WindowTopBarTitle'; /** create wrapper */ function createWrapper(props) { - return shallow( + return render( <WindowTopBarTitle manifestTitle="awesome manifest" windowId="xyz" @@ -16,28 +15,23 @@ function createWrapper(props) { } describe('WindowTopBarTitle', () => { - it('renders all needed elements', () => { - const wrapper = createWrapper(); - expect(wrapper.find('TitleTypography').length).toBe(1); - }); - - it('passes correct props to <Typography/>', () => { - const wrapper = createWrapper(); - expect(wrapper.find('TitleTypography').first().render().text()).toBe('awesome manifest'); + it('renders all needed elements with correct props', () => { + createWrapper(); + expect(screen.getByRole('heading')).toHaveTextContent('awesome manifest'); }); it('renders a Skeleton when loading', () => { - const wrapper = createWrapper({ isFetching: true }); - expect(wrapper.find('TitleTypography').dive().find(Skeleton).length).toBe(1); + createWrapper({ isFetching: true }); + expect(screen.getByRole('heading')).not.toHaveTextContent('awesome manifest'); }); it('renders an error', () => { - const wrapper = createWrapper({ error: 'some error message' }); - expect(wrapper.find('TitleTypography').render().text()).toBe('some error message'); + createWrapper({ error: 'some error message' }); + expect(screen.getByRole('heading')).toHaveTextContent('some error message'); }); it('title is configurable', () => { - expect(createWrapper({ hideWindowTitle: true }).find('TitleTypography').length).toEqual(0); - expect(createWrapper({ hideWindowTitle: true }).find('div').length).toEqual(1); + createWrapper({ hideWindowTitle: true }); + expect(screen.queryByRole('heading')).not.toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/WindowTopMenu.test.js b/__tests__/src/components/WindowTopMenu.test.js index 147240de3ced900b2992fe781b6744b69a8c0642..0cfcfa345eda2d43b97be8dbc28d45a9d794af98 100644 --- a/__tests__/src/components/WindowTopMenu.test.js +++ b/__tests__/src/components/WindowTopMenu.test.js @@ -1,63 +1,74 @@ -import { shallow } from 'enzyme'; -import Menu from '@material-ui/core/Menu'; -import WindowThumbnailSettings from '../../../src/containers/WindowThumbnailSettings'; -import WindowViewSettings from '../../../src/containers/WindowViewSettings'; +import { render, screen, within } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { WindowTopMenu } from '../../../src/components/WindowTopMenu'; /** create wrapper */ -function createWrapper(props) { - return shallow( - <WindowTopMenu - containerId="mirador" - windowId="xyz" - handleClose={() => {}} - anchorEl={null} - toggleDraggingEnabled={() => {}} - {...props} - />, +function Subject({ ...props }) { + return ( + <div> + <WindowTopMenu + windowId="xyz" + handleClose={() => {}} + toggleDraggingEnabled={() => {}} + {...props} + /> + , + </div> + ); +} + +/** create anchor element */ +function createAnchor() { + return render( + <button type="button" data-testid="menu-trigger-button">Button</button>, ); } describe('WindowTopMenu', () => { - it('renders all needed elements', () => { - const wrapper = createWrapper(); + it('renders all needed elements when open', () => { + createAnchor(); + render(<Subject anchorEl={screen.getByTestId('menu-trigger-button')} open />); - expect(wrapper.find(Menu).length).toBe(1); - expect(wrapper.find(WindowThumbnailSettings).length).toBe(1); - expect(wrapper.find(WindowViewSettings).length).toBe(1); - expect(wrapper.find('PluginHookWithHeader').length).toBe(1); - }); + expect(screen.getByRole('menu')).toBeInTheDocument(); + + const menuSections = within(screen.getByRole('menu')).getAllByRole('presentation'); + expect(menuSections).toHaveLength(2); + expect(menuSections[0]).toHaveTextContent('view'); + expect(menuSections[1]).toHaveTextContent('thumbnail'); - it('passes windowId to <WindowThumbnailSettings/>', () => { - const wrapper = createWrapper(); - expect(wrapper.find(WindowThumbnailSettings) - .first().props().windowId).toBe('xyz'); + const menuItems = screen.getAllByRole('menuitem'); + expect(menuItems).toHaveLength(5); + expect(menuItems[0]).toHaveTextContent('single'); + expect(menuItems[1]).toHaveTextContent('gallery'); + expect(menuItems[2]).toHaveTextContent('off'); + expect(menuItems[3]).toHaveTextContent('bottom'); + expect(menuItems[4]).toHaveTextContent('right'); }); - it('passses correct props to <Menu/> when no achor element given', () => { - const handleClose = jest.fn(); - const toggleDraggingEnabled = jest.fn(); - const wrapper = createWrapper({ handleClose, toggleDraggingEnabled }); - expect(wrapper.find(Menu).first().props().anchorEl).toBe(null); - expect(wrapper.find(Menu).first().props().open).toBe(false); - expect(wrapper.find(Menu).first().props().onClose).toBe(handleClose); - expect(wrapper.find(Menu).first().props().TransitionProps.onEntering) - .toBe(toggleDraggingEnabled); - expect(wrapper.find(Menu).first().props().TransitionProps.onExit) - .toBe(toggleDraggingEnabled); + it('does not display unless open', () => { + createAnchor(); + render(<Subject open={false} />); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); }); - it('passses correct props to <Menu/> when achor element given', () => { + it('fires the correct callbacks on menu close', async () => { + const user = userEvent.setup(); + createAnchor(); const handleClose = jest.fn(); const toggleDraggingEnabled = jest.fn(); - const anchorEl = {}; - const wrapper = createWrapper({ anchorEl, handleClose, toggleDraggingEnabled }); - expect(wrapper.find(Menu).first().props().anchorEl).toBe(anchorEl); - expect(wrapper.find(Menu).first().props().open).toBe(true); - expect(wrapper.find(Menu).first().props().onClose).toBe(handleClose); - expect(wrapper.find(Menu).first().props().TransitionProps.onEntering) - .toBe(toggleDraggingEnabled); - expect(wrapper.find(Menu).first().props().TransitionProps.onExit) - .toBe(toggleDraggingEnabled); + const anchorEl = screen.getByTestId('menu-trigger-button'); + + render(<Subject + anchorEl={anchorEl} + handleClose={handleClose} + open + toggleDraggingEnabled={toggleDraggingEnabled} + />); + + // click a menu item should close the menu + const menuItems = screen.getAllByRole('menuitem'); + await user.click(menuItems[0]); + expect(handleClose).toHaveBeenCalledTimes(1); + expect(toggleDraggingEnabled).toHaveBeenCalledTimes(1); }); }); diff --git a/__tests__/src/components/WindowTopMenuButton.test.js b/__tests__/src/components/WindowTopMenuButton.test.js index 2dc7c9501ecde0520ed408b900b859713e96f0f5..15d4b65f42d325a951427a68d261e999aba4f1dd 100644 --- a/__tests__/src/components/WindowTopMenuButton.test.js +++ b/__tests__/src/components/WindowTopMenuButton.test.js @@ -1,62 +1,53 @@ -import { shallow } from 'enzyme'; -import WindowTopMenu from '../../../src/containers/WindowTopMenu'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { WindowTopMenuButton } from '../../../src/components/WindowTopMenuButton'; -import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; /** create wrapper */ -function createWrapper(props) { - return shallow( +function Subject({ ...props }) { + return ( <WindowTopMenuButton windowId="xyz" + data-testid="test" classes={{ ctrlBtnSelected: 'ctrlBtnSelected' }} t={str => str} {...props} - />, + /> ); } describe('WindowTopMenuButton', () => { - it('renders all needed elements', () => { - const wrapper = createWrapper(); - expect(wrapper.find(MiradorMenuButton).length).toBe(1); - expect(wrapper.find(WindowTopMenu).length).toBe(1); + let user; + beforeEach(() => { + user = userEvent.setup(); }); - it('passes correct props to <WindowTopMenu/>', () => { - const wrapper = createWrapper(); - const props = wrapper.find(WindowTopMenu).first().props(); - const { handleMenuClose } = wrapper.instance(); - expect(props.windowId).toBe('xyz'); - expect(props.anchorEl).toBe(null); - expect(props.handleClose).toBe(handleMenuClose); + it('renders the button element', () => { + render(<Subject />); + expect(screen.getByLabelText('windowMenu')).toBeInTheDocument(); }); - it('passes correct props to <MiradorMenuButton />', () => { - const wrapper = createWrapper(); - const props = wrapper.find(MiradorMenuButton).first().props(); - const { handleMenuClick } = wrapper.instance(); - expect(props.onClick).toBe(handleMenuClick); - }); - - it('toggles anchor element in <WindowTopMenu/> on menu open/close', () => { - const wrapper = createWrapper(); - expect(wrapper.find(WindowTopMenu).first().props().anchorEl).toBe(null); + it('toggles open/close of <WindowTopMenu/> when clicked', async () => { + render(<Subject />); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); - wrapper.instance().handleMenuClick({ currentTarget: 'bubu' }); - expect(wrapper.find(WindowTopMenu).first().props().anchorEl).toBe('bubu'); + await user.click(screen.getByLabelText('windowMenu')); + expect(screen.getByRole('menu')).toBeInTheDocument(); - wrapper.instance().handleMenuClose(); - expect(wrapper.find(WindowTopMenu).first().props().anchorEl).toBe(null); + // click something else to close the menu (the windowMenu button is hidden at this point) + await user.click(screen.getAllByRole('menuitem')[0]); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); }); - it('the button has a class indicating that it is "selected" once it is clicked', () => { - const wrapper = createWrapper(); - const menuButton = wrapper.find(MiradorMenuButton).first(); + it('the open attribute of the button is null without being clicked', async () => { + render(<Subject />); + // without a click, the button is not open and therefore doesn't have aria-owns attr + expect(screen.getByLabelText('windowMenu')).not.toHaveAttribute('aria-owns'); // eslint-disable-line testing-library/no-node-access + }); - expect(wrapper.find(MiradorMenuButton).first().props().className).toEqual(null); - menuButton.props().onClick({ currentTarget: 'anElement' }); - expect(wrapper.find(MiradorMenuButton).first().props().className).toEqual('ctrlBtnSelected'); - menuButton.props().onClick({}); - expect(wrapper.find(MiradorMenuButton).first().props().className).toEqual(null); + it('the open attribute of the button is applied once it is clicked', async () => { + render(<Subject />); + await user.click(screen.getByLabelText('windowMenu')); + // when 'open' is true, aria-owns is set to the id of the window + expect(screen.getByLabelText('windowMenu')).toHaveAttribute('aria-owns', 'window-menu_xyz'); // eslint-disable-line testing-library/no-node-access }); }); diff --git a/__tests__/src/components/WindowViewSettings.test.js b/__tests__/src/components/WindowViewSettings.test.js index 61c11a2bc87c3db30873c5462d9a086c55b5553e..a46ae91d7d26e0e97d42baa6a335814a34394991 100644 --- a/__tests__/src/components/WindowViewSettings.test.js +++ b/__tests__/src/components/WindowViewSettings.test.js @@ -1,12 +1,10 @@ -import { mount } from 'enzyme'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import ListSubheader from '@material-ui/core/ListSubheader'; -import MenuItem from '@material-ui/core/MenuItem'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { WindowViewSettings } from '../../../src/components/WindowViewSettings'; /** create wrapper */ function createWrapper(props) { - return mount( + return render( <WindowViewSettings classes={{}} windowId="xyz" @@ -20,57 +18,48 @@ function createWrapper(props) { describe('WindowViewSettings', () => { it('renders all elements correctly', () => { - const wrapper = createWrapper(); - expect(wrapper.find(ListSubheader).length).toBe(1); - const labels = wrapper.find(FormControlLabel); - expect(labels.length).toBe(4); - expect(labels.at(0).props().value).toBe('single'); - expect(labels.at(1).props().value).toBe('book'); - expect(labels.at(2).props().value).toBe('scroll'); - expect(labels.at(3).props().value).toBe('gallery'); + createWrapper(); + expect(screen.getByRole('presentation', { selector: 'li' })).toBeInTheDocument(); + const menuItems = screen.queryAllByRole('menuitem'); + expect(menuItems.length).toBe(4); + expect(menuItems[0]).toHaveTextContent(/single/i); + expect(menuItems[1]).toHaveTextContent(/book/i); + expect(menuItems[2]).toHaveTextContent(/scroll/i); + expect(menuItems[3]).toHaveTextContent(/gallery/i); }); - - it('should set the correct label active (by setting the secondary color)', () => { - let wrapper = createWrapper({ windowViewType: 'single' }); - expect(wrapper.find(FormControlLabel).at(0).props().control.props.color).toEqual('secondary'); - expect(wrapper.find(FormControlLabel).at(1).props().control.props.color).not.toEqual('secondary'); - - wrapper = createWrapper({ windowViewType: 'book' }); - expect(wrapper.find(FormControlLabel).at(1).props().control.props.color).toEqual('secondary'); - - wrapper = createWrapper({ windowViewType: 'scroll' }); - expect(wrapper.find(FormControlLabel).at(2).props().control.props.color).toEqual('secondary'); - - wrapper = createWrapper({ windowViewType: 'gallery' }); - expect(wrapper.find(FormControlLabel).at(3).props().control.props.color).toEqual('secondary'); + it('single should set the correct label active (by setting the secondary color)', () => { + createWrapper({ windowViewType: 'single' }); + expect(screen.getByRole('menuitem', { name: /single/ }).querySelector('svg')).toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access + expect(screen.getByRole('menuitem', { name: /book/ }).querySelector('svg')).not.toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access }); - - it('updates state when the view config selection changes', () => { + it('book should set the correct label active (by setting the secondary color)', () => { + createWrapper({ windowViewType: 'book' }); + expect(screen.getByRole('menuitem', { name: /book/ }).querySelector('svg')).toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access + expect(screen.getByRole('menuitem', { name: /single/ }).querySelector('svg')).not.toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access + }); + it('scroll should set the correct label active (by setting the secondary color)', () => { + createWrapper({ windowViewType: 'scroll' }); + expect(screen.getByRole('menuitem', { name: /scroll/ }).querySelector('svg')).toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access + expect(screen.getByRole('menuitem', { name: /single/ }).querySelector('svg')).not.toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access + }); + it('gallery should set the correct label active (by setting the secondary color)', () => { + createWrapper({ windowViewType: 'gallery' }); + expect(screen.getByRole('menuitem', { name: /gallery/ }).querySelector('svg')).toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access + expect(screen.getByRole('menuitem', { name: /single/ }).querySelector('svg')).not.toHaveClass('MuiSvgIcon-colorSecondary'); // eslint-disable-line testing-library/no-node-access + }); + it('updates state when the view config selection changes', async () => { const setWindowViewType = jest.fn(); - const wrapper = createWrapper({ setWindowViewType }); - wrapper.find(MenuItem).at(0).simulate('click'); + createWrapper({ setWindowViewType }); + const user = userEvent.setup(); + const menuItems = screen.queryAllByRole('menuitem'); + expect(menuItems.length).toBe(4); + await user.click(menuItems[0]); expect(setWindowViewType).toHaveBeenCalledWith('xyz', 'single'); - wrapper.find(MenuItem).at(1).simulate('click'); + await user.click(menuItems[1]); expect(setWindowViewType).toHaveBeenCalledWith('xyz', 'book'); - wrapper.find(MenuItem).at(2).simulate('click'); + await user.click(menuItems[2]); expect(setWindowViewType).toHaveBeenCalledWith('xyz', 'scroll'); - wrapper.find(MenuItem).at(3).simulate('click'); + await user.click(menuItems[3]); expect(setWindowViewType).toHaveBeenCalledWith('xyz', 'gallery'); }); - - it('sets autofocus on the selected MenuItem', () => { - const wrapper = mount( - <WindowViewSettings - classes={{}} - windowId="xyz" - setWindowViewType={() => {}} - viewTypes={['single', 'book', 'scroll', 'gallery']} - windowViewType="book" - />, - ); - - expect( - wrapper.find(MenuItem).at(1).prop('autoFocus'), - ).toEqual(true); - }); }); diff --git a/__tests__/src/components/WindowViewer.test.js b/__tests__/src/components/WindowViewer.test.js index 6c7af5c441750a8f78d20b1be0f5933c994bab29..28564de7327b1b9eb65f20de459bae33c284f095 100644 --- a/__tests__/src/components/WindowViewer.test.js +++ b/__tests__/src/components/WindowViewer.test.js @@ -1,31 +1,20 @@ -import { shallow } from 'enzyme'; +import { render, screen } from 'test-utils'; import { WindowViewer } from '../../../src/components/WindowViewer'; -import WindowCanvasNavigationControls from '../../../src/containers/WindowCanvasNavigationControls'; /** create wrapper */ -function createWrapper(props, suspenseFallback) { - return shallow( +function createWrapper(props) { + return render( <WindowViewer - windowId="xyz" {...props} />, - { suspenseFallback }, ); } describe('WindowViewer', () => { - let wrapper; - describe('when lazy imorts have not loaded', () => { - it('renders fallback', () => { - wrapper = createWrapper({}, true); - expect(wrapper.find('div').length).toBe(1); - }); - }); - describe('when lazy imorts have loaded', () => { - it('renders expected components', () => { - wrapper = createWrapper({}, false); - expect(wrapper.find('lazy').props().windowId).toBe('xyz'); - expect(wrapper.find(WindowCanvasNavigationControls).props().windowId).toBe('xyz'); + describe('when lazy imports have loaded', () => { + it('renders openseadragon', async () => { + createWrapper({}); + await screen.findByRole('img', { selector: 'canvas' }); }); }); }); diff --git a/__tests__/src/components/Workspace.test.js b/__tests__/src/components/Workspace.test.js index fc71c76c8330ba9b8717b3123290180c07abb762..3f1fd853e79f7aec16a9fad1ab6b05b789675b1b 100644 --- a/__tests__/src/components/Workspace.test.js +++ b/__tests__/src/components/Workspace.test.js @@ -1,135 +1,146 @@ -import { shallow } from 'enzyme'; -import Typography from '@material-ui/core/Typography'; -import WorkspaceMosaic from '../../../src/containers/WorkspaceMosaic'; -import WorkspaceElastic from '../../../src/containers/WorkspaceElastic'; -import Window from '../../../src/containers/Window'; +import { + render, screen, fireEvent, waitFor, +} from 'test-utils'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; + import { Workspace } from '../../../src/components/Workspace'; -import { IIIFDropTarget } from '../../../src/components/IIIFDropTarget'; /** * Utility function to create a Worksapce * component with all required props set */ function createWrapper(props) { - return shallow( - <Workspace - classes={{}} - isWorkspaceControlPanelVisible - windowIds={['1', '2']} - workspaceId="foo" - workspaceType="mosaic" - t={k => k} - {...props} - />, + return render( + <DndProvider backend={HTML5Backend}> + <Workspace + classes={{}} + isWorkspaceControlPanelVisible + windowIds={['1', '2']} + workspaceId="foo" + workspaceType="mosaic" + t={k => k} + {...props} + /> + </DndProvider>, + { + preloadedState: { + windows: { 1: {}, 2: {} }, + workspace: { + viewportPosition: { + height: 10, width: 10, x: 0, y: 0, + }, + }, + }, + }, ); } +/* eslint-disable testing-library/no-container, testing-library/no-node-access */ describe('Workspace', () => { describe('if workspace type is elastic', () => { it('should render <WorkspaceElastic/> properly', () => { - const wrapper = createWrapper({ workspaceType: 'elastic' }); - - expect(wrapper.matchesElement( - <IIIFDropTarget> - <div className="mirador-workspace-viewport mirador-workspace-with-control-panel"> - <Typography>miradorViewer</Typography> - <WorkspaceElastic /> - </div> - </IIIFDropTarget>, - )).toBe(true); + const { container } = createWrapper({ workspaceType: 'elastic' }); + + expect(screen.getByRole('heading', { name: 'miradorViewer' })).toBeInTheDocument(); + + expect(container.querySelector('.mirador-workspace.react-draggable')).toBeInTheDocument(); }); }); describe('if workspace type is mosaic', () => { it('should render <WorkspaceMosaic/> properly', () => { - const wrapper = createWrapper(); - - expect(wrapper.matchesElement( - <IIIFDropTarget> - <div className="mirador-workspace-viewport mirador-workspace-with-control-panel"> - <Typography>miradorViewer</Typography> - <WorkspaceMosaic /> - </div> - </IIIFDropTarget>, - )).toBe(true); + const { container } = createWrapper(); + + expect(screen.getByRole('heading', { name: 'miradorViewer' })).toBeInTheDocument(); + + expect(container.querySelector('.mirador-mosaic')).toBeInTheDocument(); + expect(container.querySelector('.drop-target-container')).toBeInTheDocument(); }); }); describe('if workspace type is unknown', () => { it('should render <Window/> components as list', () => { - const wrapper = createWrapper({ workspaceType: 'bubu' }); - expect(wrapper.matchesElement( - <IIIFDropTarget> - <div className="mirador-workspace-viewport mirador-workspace-with-control-panel"> - <Typography>miradorViewer</Typography> - <Window windowId="1" /> - <Window windowId="2" /> - </div> - </IIIFDropTarget>, - )).toBe(true); + createWrapper({ workspaceType: 'bubu' }); + + expect(screen.getByRole('heading', { name: 'miradorViewer' })).toBeInTheDocument(); + expect(screen.getAllByLabelText('window')).toHaveLength(2); }); }); describe('if any windows are maximized', () => { it('should render only maximized <Window/> components', () => { - const wrapper = createWrapper({ maximizedWindowIds: ['1'] }); - expect(wrapper.matchesElement( - <IIIFDropTarget> - <div className="mirador-workspace-viewport mirador-workspace-with-control-panel"> - <Typography>miradorViewer</Typography> - <Window windowId="1" className="mirador-workspace-maximized-window" /> - </div> - </IIIFDropTarget>, - )).toBe(true); + createWrapper({ maximizedWindowIds: ['1'] }); + + expect(screen.getByRole('heading', { name: 'miradorViewer' })).toBeInTheDocument(); + expect(screen.getByLabelText('window')).toHaveAttribute('id', '1'); }); }); describe('if there are no windows', () => { it('should render placeholder content', () => { - const wrapper = createWrapper({ windowIds: [] }); + createWrapper({ windowIds: [] }); - expect(wrapper.find(Typography).at(1).matchesElement( - <Typography>welcome</Typography>, - )).toBe(true); + expect(screen.getByRole('heading', { name: 'miradorViewer' })).toBeInTheDocument(); + expect(screen.getByText('welcome')).toHaveClass('MuiTypography-h1'); }); }); describe('when the workspace control panel is displayed', () => { it('has the *-with-control-panel class applied', () => { - const wrapper = createWrapper(); + const { container } = createWrapper(); - expect(wrapper.find('.mirador-workspace-with-control-panel').length).toBe(1); + expect(container.querySelector('.mirador-workspace-with-control-panel')).toBeInTheDocument(); }); }); describe('when the workspace control panel is not displayed', () => { it('does not have the *-with-control-panel class applied', () => { - const wrapper = createWrapper({ isWorkspaceControlPanelVisible: false }); + const { container } = createWrapper({ isWorkspaceControlPanelVisible: false }); - expect(wrapper.find('.mirador-workspace-with-control-panel').length).toBe(0); + expect(container.querySelector('.mirador-workspace-with-control-panel')).not.toBeInTheDocument(); }); }); describe('drag and drop', () => { - it('adds a new window', () => { - const canvasId = 'canvasId'; - const manifestId = 'manifest.json'; - const manifestJson = { data: '123' }; + it('adds a new catalog entry from a manifest', async () => { + const manifestJson = '{ "data": "123" }'; const addWindow = jest.fn(); - const wrapper = createWrapper({ addWindow }); + const { container } = createWrapper({ addWindow }); + const dropTarget = container.querySelector('.mirador-workspace-with-control-panel'); + + const file = new File([manifestJson], 'manifest.json', { type: 'application/json' }); + const dataTransfer = { + files: [file], + types: ['Files'], + }; - wrapper.find(IIIFDropTarget).simulate('drop', { canvasId, manifestId, manifestJson }); + fireEvent.dragStart(dropTarget, { dataTransfer }); + fireEvent.dragEnter(dropTarget, { dataTransfer }); + fireEvent.dragOver(dropTarget, { dataTransfer }); + fireEvent.drop(dropTarget, { dataTransfer }); - expect(addWindow).toHaveBeenCalledWith({ canvasId, manifest: manifestJson, manifestId }); + await waitFor(() => expect(addWindow).toHaveBeenCalledWith({ manifest: manifestJson, manifestId: expect.stringMatching(/^[0-9a-f-]+$/) })); }); - it('is a no-op if allowNewWindows is off', () => { - const canvasId = 'canvasId'; - const manifestId = 'manifest.json'; - const manifestJson = { data: '123' }; + it('adds a new catalog entry from a IIIF drag and drop icon', () => { + const manifestJson = '{ "data": "123" }'; const addWindow = jest.fn(); - const wrapper = createWrapper({ addWindow, allowNewWindows: false }); - wrapper.find(IIIFDropTarget).simulate('drop', { canvasId, manifestId, manifestJson }); + + const { container } = createWrapper({ addWindow, allowNewWindows: false }); + + const dropTarget = container.querySelector('.mirador-workspace-with-control-panel'); + + const file = new File([manifestJson], 'manifest.json', { type: 'application/json' }); + const dataTransfer = { + files: [file], + types: ['Files'], + }; + + fireEvent.dragStart(dropTarget, { dataTransfer }); + fireEvent.dragEnter(dropTarget, { dataTransfer }); + fireEvent.dragOver(dropTarget, { dataTransfer }); + fireEvent.drop(dropTarget, { dataTransfer }); expect(addWindow).not.toHaveBeenCalled(); }); diff --git a/__tests__/src/components/WorkspaceAdd.test.js b/__tests__/src/components/WorkspaceAdd.test.js index ce61aa599988608409d8d1881e3847d52b3826e1..a9f44a31e6b1dcd432dfd2d31611a64105f6caa6 100644 --- a/__tests__/src/components/WorkspaceAdd.test.js +++ b/__tests__/src/components/WorkspaceAdd.test.js @@ -1,128 +1,160 @@ -import { shallow } from 'enzyme'; -import AppBar from '@material-ui/core/AppBar'; -import Drawer from '@material-ui/core/Drawer'; -import Fab from '@material-ui/core/Fab'; -import Typography from '@material-ui/core/Typography'; +import { + render, screen, fireEvent, waitFor, +} from 'test-utils'; +import userEvent from '@testing-library/user-event'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; + import { WorkspaceAdd } from '../../../src/components/WorkspaceAdd'; -import ManifestListItem from '../../../src/containers/ManifestListItem'; -import ManifestForm from '../../../src/containers/ManifestForm'; -import { IIIFDropTarget } from '../../../src/components/IIIFDropTarget'; +import manifestFixture001 from '../../fixtures/version-2/001.json'; +import manifestFixture002 from '../../fixtures/version-2/002.json'; /** create wrapper */ function createWrapper(props) { - return shallow( - <WorkspaceAdd - setWorkspaceAddVisibility={() => {}} - catalog={[ - { manifestId: 'bar' }, - { manifestId: 'foo' }, - ]} - classes={{}} - t={str => str} - {...props} - />, + return render( + <DndProvider backend={HTML5Backend}> + <WorkspaceAdd + setWorkspaceAddVisibility={() => {}} + catalog={[ + { manifestId: 'bar' }, + { manifestId: 'foo' }, + ]} + classes={{}} + t={str => str} + {...props} + /> + </DndProvider>, + { preloadedState: { manifests: { bar: { id: 'bar', isFetching: false, json: manifestFixture001 }, foo: { id: 'foo', isFetching: false, json: manifestFixture002 } } } }, ); } describe('WorkspaceAdd', () => { it('renders a list item for each manifest in the state', () => { - const wrapper = createWrapper(); - expect(wrapper.find(ManifestListItem).length).toBe(2); + createWrapper(); + + expect(screen.getAllByRole('listitem')).toHaveLength(2); }); it('focuses on the first manifest item', () => { - const el = { focus: jest.fn() }; - const wrapper = createWrapper(); - expect(wrapper.find(ManifestListItem).at(1).prop('buttonRef')).toBe(undefined); - wrapper.find(ManifestListItem).at(0).prop('buttonRef')(el); + createWrapper(); - expect(el.focus).toHaveBeenCalled(); + expect(screen.getByRole('button', { name: 'Bodleian Library Human Freaks 2 (33)' })).toHaveFocus(); }); it('without manifests, renders an empty message', () => { - const wrapper = createWrapper({ catalog: [] }); - expect(wrapper.find(ManifestListItem).length).toEqual(0); - expect(wrapper.find(Typography).first().children().text()).toEqual('emptyResourceList'); + createWrapper({ catalog: [] }); + + expect(screen.queryByRole('listitem')).not.toBeInTheDocument(); + expect(screen.getByText('emptyResourceList')).toBeInTheDocument(); }); - it('toggles the workspace visibility', () => { + it('toggles the workspace visibility', async () => { + const user = userEvent.setup(); const setWorkspaceAddVisibility = jest.fn(); - const wrapper = createWrapper({ setWorkspaceAddVisibility }); + createWrapper({ setWorkspaceAddVisibility }); + + await user.click(screen.getByRole('button', { name: 'Bodleian Library Human Freaks 2 (33)' })); - wrapper.find(ManifestListItem).first().props().handleClose(); expect(setWorkspaceAddVisibility).toHaveBeenCalledWith(false); }); - it('has a button to add new resources', () => { - const wrapper = createWrapper(); + it('has a button to add new resources', async () => { + const user = userEvent.setup(); + createWrapper(); - expect(wrapper.find(Fab).length).toBe(1); - wrapper.find(Fab).simulate('click'); - expect(wrapper.state().addResourcesOpen).toBe(true); - expect(wrapper.find(Fab).props().disabled).toBe(true); - }); + const fab = screen.getByRole('button', { name: 'addResource' }); - it('has a toggle-able drawer to add new resources', () => { - const wrapper = createWrapper(); - wrapper.setState({ addResourcesOpen: true }); + expect(fab).toBeInTheDocument(); + await user.click(fab); - expect(wrapper.find(Drawer).props().open).toBe(true); - expect(wrapper.find(Drawer).find(Typography).dive().dive() - .text()).toBe('addResource'); + expect(fab).toBeDisabled(); + expect(screen.getByRole('textbox')).toBeInTheDocument(); - wrapper.find(Drawer).find(AppBar).simulate('click'); - expect(wrapper.find(Drawer).find(Typography).props().open).not.toBe(true); + await user.click(screen.getByRole('button', { name: 'closeAddResourceForm' })); + + expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); }); - it('passes a submit action through to the form', () => { - const wrapper = createWrapper(); - wrapper.setState({ addResourcesOpen: true }); + it('hides the form on submit', async () => { + const user = userEvent.setup(); + createWrapper(); + + await user.click(screen.getByRole('button', { name: 'addResource' })); - expect(wrapper.find(Drawer).find(ManifestForm).length).toBe(1); - wrapper.find(Drawer).find(ManifestForm).props().onSubmit(); - expect(wrapper.find(Drawer).props().open).toBe(false); + await user.type(screen.getByRole('textbox'), 'abc'); + await user.click(screen.getByRole('button', { name: 'fetchManifest' })); + + expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); }); - it('scrolls to the top after an item is added', () => { - const ref = { current: { scrollTo: jest.fn() } }; - const wrapper = createWrapper(); - wrapper.instance().ref = ref; - wrapper.instance().onSubmit(); + it('scrolls to the top after an item is added', async () => { + const user = userEvent.setup(); + const { container } = createWrapper(); + + const scrollTo = jest.fn(); - expect(ref.current.scrollTo).toHaveBeenCalledWith({ behavior: 'smooth', left: 0, top: 0 }); + jest.spyOn(container.querySelector('.mirador-workspace-add'), 'scrollTo').mockImplementation(scrollTo); // eslint-disable-line testing-library/no-node-access, testing-library/no-container + + await user.click(screen.getByRole('button', { name: 'addResource' })); + + await user.type(screen.getByRole('textbox'), 'abc'); + await user.click(screen.getByRole('button', { name: 'fetchManifest' })); + + expect(scrollTo).toHaveBeenCalledWith({ behavior: 'smooth', left: 0, top: 0 }); }); - it('passes a cancel action through to the form', () => { - const wrapper = createWrapper(); - wrapper.setState({ addResourcesOpen: true }); + it('hides the form on cancel action', async () => { + const user = userEvent.setup(); + createWrapper(); + + await user.click(screen.getByRole('button', { name: 'addResource' })); - expect(wrapper.find(Drawer).find(ManifestForm).length).toBe(1); - wrapper.find(Drawer).find(ManifestForm).props().onCancel(); - expect(wrapper.find(Drawer).props().open).toBe(false); + await user.type(screen.getByRole('textbox'), 'abc'); + await user.click(screen.getByRole('button', { name: 'cancel' })); + + expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); }); describe('drag and drop', () => { - it('adds a new catalog entry from a manifest', () => { - const manifestId = 'manifest.json'; - const manifestJson = { data: '123' }; + it('adds a new catalog entry from a manifest', async () => { + const manifestJson = '{ "data": "123" }'; const addResource = jest.fn(); - const wrapper = createWrapper({ addResource }); + createWrapper({ addResource }); + const dropTarget = screen.getByRole('list'); - wrapper.find(IIIFDropTarget).simulate('drop', { manifestId, manifestJson }); + const file = new File([manifestJson], 'manifest.json', { type: 'application/json' }); + const dataTransfer = { + files: [file], + types: ['Files'], + }; - expect(addResource).toHaveBeenCalledWith(manifestId, manifestJson, { provider: 'file' }); + fireEvent.dragStart(dropTarget, { dataTransfer }); + fireEvent.dragEnter(dropTarget, { dataTransfer }); + fireEvent.dragOver(dropTarget, { dataTransfer }); + fireEvent.drop(dropTarget, { dataTransfer }); + + await waitFor(() => expect(addResource).toHaveBeenCalledWith(expect.stringMatching(/^[0-9a-f-]+$/), manifestJson, { provider: 'file' })); }); - it('adds a new catalog entry from a manifestId', () => { + it('adds a new catalog entry from a IIIF drag and drop icon', () => { const manifestId = 'manifest.json'; const addResource = jest.fn(); - const wrapper = createWrapper({ addResource }); + createWrapper({ addResource }); + const dropTarget = screen.getByRole('list'); + + const dataTransfer = { + getData: () => 'https://iiif.io/?manifest=manifest.json', + types: ['Url'], + }; - wrapper.find(IIIFDropTarget).simulate('drop', { manifestId }); + fireEvent.dragStart(dropTarget, { dataTransfer }); + fireEvent.dragEnter(dropTarget, { dataTransfer }); + fireEvent.dragOver(dropTarget, { dataTransfer }); + fireEvent.drop(dropTarget, { dataTransfer }); expect(addResource).toHaveBeenCalledWith(manifestId); }); diff --git a/__tests__/src/components/WorkspaceAddButton.test.js b/__tests__/src/components/WorkspaceAddButton.test.js index 820906f0a576e97dda16821e2cfbca7d2c161ffe..1740b80a4426207b06aeece3e4d1755481de8d84 100644 --- a/__tests__/src/components/WorkspaceAddButton.test.js +++ b/__tests__/src/components/WorkspaceAddButton.test.js @@ -1,13 +1,10 @@ -import { shallow } from 'enzyme'; -import Fab from '@material-ui/core/Fab'; -import Typography from '@material-ui/core/Typography'; -import AddIcon from '@material-ui/icons/AddSharp'; -import CloseIcon from '@material-ui/icons/CloseSharp'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { WorkspaceAddButton } from '../../../src/components/WorkspaceAddButton'; /** create wrapper */ function createWrapper(props) { - return shallow( + return render( <WorkspaceAddButton classes={{}} setWorkspaceAddVisibility={() => {}} @@ -19,52 +16,30 @@ function createWrapper(props) { } describe('WorkspaceAddButton', () => { - it('renders a button to open the load window area', () => { + it('renders a button to open the load window area', async () => { + const user = userEvent.setup(); const setWorkspaceAddVisibility = jest.fn(); - const wrapper = createWrapper({ isWorkspaceAddVisible: false, setWorkspaceAddVisibility }); + createWrapper({ isWorkspaceAddVisible: false, setWorkspaceAddVisibility }); - expect(wrapper.find(AddIcon).length).toBe(1); + await user.click(screen.getByRole('button', { name: 'startHere' })); - wrapper.find(Fab).simulate('click'); expect(setWorkspaceAddVisibility).toHaveBeenCalledWith(true); }); - it('renders additional text for an empty workspace', () => { - const wrapper = createWrapper({ emptyWorkspace: true, isWorkspaceAddVisible: false }); - expect(wrapper.find(Fab).matchesElement( - <Fab> - <AddIcon /> - startHere - </Fab>, - )).toBe(true); - expect(wrapper.find(Fab).prop('aria-label')).toMatch('startHere'); - }); - - it('renders a button to close the load window area', () => { + it('renders a button to close the load window area', async () => { + const user = userEvent.setup(); const setWorkspaceAddVisibility = jest.fn(); - const wrapper = createWrapper({ isWorkspaceAddVisible: true, setWorkspaceAddVisibility }); - - expect(wrapper.find(CloseIcon).length).toBe(1); + createWrapper({ isWorkspaceAddVisible: true, setWorkspaceAddVisibility }); - wrapper.find(Fab).simulate('click'); + await user.click(screen.getByRole('button', { name: 'closeAddResourceMenu' })); expect(setWorkspaceAddVisibility).toHaveBeenCalledWith(false); - expect(wrapper.find(Fab).prop('aria-label')).toMatch('closeAddResourceMenu'); }); - describe('when the useExtendedFab prop is false', () => { - it('does not have the startHere Typography ', () => { - const wrapper = createWrapper({ useExtendedFab: false }); - - expect(wrapper.find(Typography).length).toEqual(0); - expect(wrapper.find(Fab).prop('aria-label')).toMatch('addResource'); - }); - - it('the Fab does not have extended variant prop', () => { - const extendedWrapper = createWrapper({ useExtendedFab: true }); - const wrapper = createWrapper({ useExtendedFab: false }); + describe('when the useExtendedFab prop is set', () => { + it('is styled using the extended variant', () => { + createWrapper({ useExtendedFab: true }); - expect(extendedWrapper.find(Fab).props().variant).toBe('extended'); - expect(wrapper.find(Fab).props().variant).toEqual('circular'); + expect(screen.getByRole('button', { name: 'startHere' })).toHaveClass('MuiFab-extended'); }); }); }); diff --git a/__tests__/src/components/WorkspaceArea.test.js b/__tests__/src/components/WorkspaceArea.test.js index 679439458a93a965b1815908e5e5031413facf82..1648a31daf5a94a0ef8ed6b16990041ddceb0a27 100644 --- a/__tests__/src/components/WorkspaceArea.test.js +++ b/__tests__/src/components/WorkspaceArea.test.js @@ -1,55 +1,60 @@ -import { shallow } from 'enzyme'; -import WorkspaceControlPanel from '../../../src/containers/WorkspaceControlPanel'; -import Workspace from '../../../src/containers/Workspace'; -import WorkspaceAdd from '../../../src/containers/WorkspaceAdd'; -import ErrorDialog from '../../../src/containers/ErrorDialog'; -import BackgroundPluginArea from '../../../src/containers/BackgroundPluginArea'; +import { render, screen, within } from 'test-utils'; +import { DndProvider } from 'react-dnd'; +import { TestBackend } from 'react-dnd-test-backend'; + import { WorkspaceArea } from '../../../src/components/WorkspaceArea'; /** */ function createWrapper(props) { - return shallow( - <WorkspaceArea - isWorkspaceControlPanelVisible - classes={{}} - lang="en" - t={k => k} - {...props} - />, + return render( + <DndProvider backend={TestBackend}> + <WorkspaceArea + isWorkspaceControlPanelVisible + classes={{}} + lang="en" + t={k => k} + {...props} + /> + </DndProvider>, ); } describe('WorkspaceArea', () => { it('should render outer element correctly', () => { - const wrapper = createWrapper(); - expect(wrapper.find('main.mirador-viewer').length).toBe(1); - expect(wrapper.find('main').prop('lang')).toEqual('en'); + createWrapper(); + + expect(screen.getByRole('main')).toHaveClass('mirador-viewer'); + expect(screen.getByRole('main')).toHaveAttribute('lang', 'en'); }); - it('should render all needed elements in order', () => { - const wrapper = createWrapper(); - expect(wrapper.containsMatchingElement( - <> - <WorkspaceControlPanel /> - <main> - <Workspace /> - <ErrorDialog /> - <BackgroundPluginArea /> - </main> - </>, - )).toBeTruthy(); + it('should render all needed elements', () => { + const { container } = createWrapper(); + + expect(screen.getByRole('button', { name: 'listAllOpenWindows' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'workspaceMenu' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'workspaceOptions' })).toBeInTheDocument(); + + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('miradorViewer'); + expect(screen.getByRole('main')).toHaveTextContent('welcome'); + + expect(container.querySelector('.mirador-background-plugin-area')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access, testing-library/no-container }); it('should not render WorkspaceControlPanel when isWorkspaceControlPanelVisible is false', () => { - const wrapper = createWrapper({ isWorkspaceControlPanelVisible: false }); + createWrapper({ isWorkspaceControlPanelVisible: false }); - expect(wrapper.find(WorkspaceControlPanel).length).toBe(0); + expect(screen.queryByRole('button', { name: 'listAllOpenWindows' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'workspaceMenu' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'workspaceOptions' })).not.toBeInTheDocument(); }); describe('with isWorkspaceAddVisible', () => { - const wrapper = createWrapper({ isWorkspaceAddVisible: true }); + it('should render WorkspaceAdd when isWorkspaceAddVisible is true', () => { + createWrapper({ isWorkspaceAddVisible: true }); - expect(wrapper.find(Workspace).length).toBe(0); - expect(wrapper.find(WorkspaceAdd).length).toBe(1); + expect(screen.queryByRole('heading', { level: 1, name: 'miradorViewer' })).not.toBeInTheDocument(); + expect(screen.getByRole('main')).toHaveTextContent('emptyResourceList'); + expect(within(screen.getByRole('main')).getByRole('button', { name: 'addResource' })).toBeInTheDocument(); + }); }); }); diff --git a/__tests__/src/components/WorkspaceControlPanel.test.js b/__tests__/src/components/WorkspaceControlPanel.test.js index 5da5dae955cf05121c23e1d856a6a9a887251d48..d0480c56e6715126de1689867260bea15da757d3 100644 --- a/__tests__/src/components/WorkspaceControlPanel.test.js +++ b/__tests__/src/components/WorkspaceControlPanel.test.js @@ -1,32 +1,22 @@ -import { shallow } from 'enzyme'; -import AppBar from '@material-ui/core/AppBar'; -import createStore from '../../../src/state/createStore'; -import * as actions from '../../../src/state/actions'; -import WorkspaceAddButton from '../../../src/containers/WorkspaceAddButton'; -import WorkspaceControlPanelButtons from '../../../src/containers/WorkspaceControlPanelButtons'; -import Branding from '../../../src/containers/Branding'; +import { render, screen } from 'test-utils'; + import { WorkspaceControlPanel } from '../../../src/components/WorkspaceControlPanel'; -import fixture from '../../fixtures/version-2/002.json'; describe('WorkspaceControlPanel', () => { - let wrapper; - const store = createStore(); beforeEach(() => { - store.dispatch(actions.receiveManifest('foo', fixture)); - store.dispatch(actions.receiveManifest('bar', fixture)); - wrapper = shallow( + render( <WorkspaceControlPanel - classes={{}} - store={store} - t={k => k} + t={key => key} />, ); }); it('renders without an error', () => { - expect(wrapper.find(AppBar).length).toBe(1); - expect(wrapper.find(WorkspaceAddButton).length).toBe(1); - expect(wrapper.find(WorkspaceControlPanelButtons).length).toBe(1); - expect(wrapper.find(Branding).length).toBe(1); + expect(screen.getByRole('navigation', { name: 'workspaceNavigation' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'addResource' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'listAllOpenWindows' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'workspaceMenu' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'workspaceOptions' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'aboutMirador' })).toHaveAttribute('href', 'https://projectmirador.org'); }); }); diff --git a/__tests__/src/components/WorkspaceControlPanelButtons.test.js b/__tests__/src/components/WorkspaceControlPanelButtons.test.js index 5f01250727d1c9dbca31531eacf9d0d805ee8728..1ac87e58994f6047b82c60d4a4df195381f2cd51 100644 --- a/__tests__/src/components/WorkspaceControlPanelButtons.test.js +++ b/__tests__/src/components/WorkspaceControlPanelButtons.test.js @@ -1,18 +1,15 @@ -import { shallow } from 'enzyme'; -import WorkspaceMenuButton from '../../../src/containers/WorkspaceMenuButton'; -import FullScreenButton from '../../../src/containers/FullScreenButton'; +import { render, screen } from 'test-utils'; + import { WorkspaceControlPanelButtons } from '../../../src/components/WorkspaceControlPanelButtons'; -import { PluginHook } from '../../../src/components/PluginHook'; describe('WorkspaceControlPanelButtons', () => { - let wrapper; beforeEach(() => { - wrapper = shallow(<WorkspaceControlPanelButtons />); + render(<WorkspaceControlPanelButtons />); }); it('render all needed elements', () => { - expect(wrapper.find(WorkspaceMenuButton).length).toBe(1); - expect(wrapper.find(FullScreenButton).length).toBe(1); - expect(wrapper.find(PluginHook).length).toBe(1); + expect(screen.getByRole('button', { name: 'listAllOpenWindows' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'workspaceMenu' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'workspaceOptions' })).toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/WorkspaceElastic.test.js b/__tests__/src/components/WorkspaceElastic.test.js index 0d436f7d4b1fbfd29444c55b8111ae40b9962dff..e88a99dfbce3e0336debb9f9b4271c1492ec5016 100644 --- a/__tests__/src/components/WorkspaceElastic.test.js +++ b/__tests__/src/components/WorkspaceElastic.test.js @@ -1,16 +1,16 @@ -import { shallow } from 'enzyme'; -import { Rnd } from 'react-rnd'; -import ResizeObserver from 'react-resize-observer'; +import { fireEvent, render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; + import WorkspaceElastic from '../../../src/components/WorkspaceElastic'; -import WorkspaceElasticWindow from '../../../src/containers/WorkspaceElasticWindow'; /** create wrapper */ -function createWrapper(props) { - return shallow( +function createWrapper({ elasticLayout = {}, ...props }) { + return render( <WorkspaceElastic classes={{}} - elasticLayout={{}} + elasticLayout={elasticLayout} workspace={{ + draggingEnabled: true, focusedWindowId: '2', height: 5000, viewportPosition: { @@ -24,9 +24,18 @@ function createWrapper(props) { updateElasticWindowLayout={() => {}} {...props} />, + { + preloadedState: { + companionWindows: {}, + elasticLayout, + windows: { 1: { companionWindowIds: [] }, 2: { companionWindowIds: [] } }, + workspace: { draggingEnabled: true }, + }, + }, ); } +/* eslint-disable testing-library/no-node-access, testing-library/no-container */ describe('WorkspaceElastic', () => { const elasticLayout = { 1: { @@ -44,52 +53,62 @@ describe('WorkspaceElastic', () => { y: 25, }, }; - let wrapper; - beforeEach(() => { - wrapper = createWrapper({ elasticLayout }); - }); + it('should render properly with an initialValue', () => { - expect(wrapper.find(WorkspaceElasticWindow).length).toBe(2); + createWrapper({ elasticLayout }); + expect(screen.getAllByLabelText('window')).toHaveLength(2); }); describe('workspace behaviour', () => { - it('when workspace itself is dragged', () => { + it('when workspace itself is dragged', async () => { + const user = userEvent.setup(); const mockDragStop = jest.fn(); - wrapper = createWrapper({ + const { container } = createWrapper({ elasticLayout, setWorkspaceViewportPosition: mockDragStop, }); - wrapper - .find(Rnd) - .at(0) - .props() - .onDragStop('myevent', { - x: 200, - y: 200, - }); + + container.getBoundingClientRect = () => ({ + left: -2500, + offsetHeight: 5000, + offsetWidth: 5000, + top: -2500, + }); + const el = container.querySelector('.mirador-workspace.react-draggable'); + + const coords = { + clientX: 400, + clientY: 300, + }; + + await user.pointer([ + { coords: { clientX: 0, clientY: 0 }, keys: '[MouseLeft>]', target: el }, + { coords }, + { coords, keys: '[/MouseLeft]', target: el }, + ]); + expect(mockDragStop).toHaveBeenCalledWith({ - x: -2700, - y: -2700, + x: -1 * (400 - 20), + y: -1 * (300 - 20), }); }); it('when workspace itself is resized', () => { const mockResize = jest.fn(); - wrapper = createWrapper({ + const { container } = createWrapper({ elasticLayout, setWorkspaceViewportDimensions: mockResize, }); - wrapper - .find(ResizeObserver) - .at(0) - .props() - .onResize({ - height: 500, - width: 500, - }); + container.firstChild.getBoundingClientRect = () => ({ + height: 500, + width: 800, + }); + + fireEvent(window, new Event('resize')); + expect(mockResize).toHaveBeenCalledWith({ height: 500, - width: 500, + width: 800, }); }); }); diff --git a/__tests__/src/components/WorkspaceElasticWindow.test.js b/__tests__/src/components/WorkspaceElasticWindow.test.js index 3a5673a32420c281b4cbd49157f8957ac327c5fd..03f565afd16baebc97bd1774e00dc388eb0273df 100644 --- a/__tests__/src/components/WorkspaceElasticWindow.test.js +++ b/__tests__/src/components/WorkspaceElasticWindow.test.js @@ -1,27 +1,34 @@ -import { shallow } from 'enzyme'; -import { Rnd } from 'react-rnd'; +import userEvent from '@testing-library/user-event'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { render } from 'test-utils'; + import WorkspaceElasticWindow from '../../../src/components/WorkspaceElasticWindow'; /** create wrapper */ function createWrapper(props) { - return shallow( - <WorkspaceElasticWindow - layout={{}} - workspace={{ - focusedWindowId: '2', - height: 5000, - viewportPosition: { - x: 20, - y: 20, - }, - width: 5000, - }} - updateElasticWindowLayout={() => {}} - {...props} - />, + return render( + <DndProvider backend={HTML5Backend}> + <WorkspaceElasticWindow + layout={{}} + workspace={{ + focusedWindowId: '2', + height: 5000, + viewportPosition: { + x: 20, + y: 20, + }, + width: 5000, + }} + updateElasticWindowLayout={() => {}} + {...props} + /> + </DndProvider>, + { preloadedState: { companionWindows: {}, windows: { 1: { companionWindowIds: [] } }, workspace: {} } }, ); } +/* eslint-disable testing-library/no-container, testing-library/no-node-access */ describe('WorkspaceElasticWindow', () => { const layout = { height: 200, @@ -31,72 +38,86 @@ describe('WorkspaceElasticWindow', () => { y: 20, }; - let wrapper; - beforeEach(() => { - wrapper = createWrapper({ layout }); - }); it('should render properly with an initialValue', () => { - expect(wrapper - .find(Rnd) - .prop('size')) - .toEqual({ - height: 200, - width: 200, - }); - expect(wrapper - .find(Rnd) - .prop('position')) - .toEqual({ - x: 2520, - y: 2520, - }); + const { container } = createWrapper({ layout }); + const el = container.firstChild; + + expect(el).toHaveClass('react-draggable'); + expect(el).toHaveStyle({ height: '200px', transform: 'translate(5040px,5040px)', width: '200px' }); }); - describe('focused window', () => { - it('adds a class to the focused window', () => { - wrapper = createWrapper({ focused: true, layout }); - expect(wrapper.find(Rnd).hasClass('mirador-workspace-focused-window')); + describe('focuses the window', () => { + it('calls focusWindow when clicked', async () => { + const user = userEvent.setup(); + const mockFocusWindow = jest.fn(); + const { container } = createWrapper({ focusWindow: mockFocusWindow, layout }); + const topBar = container.querySelector('.mirador-window-top-bar'); + await user.click(topBar); + + expect(mockFocusWindow).toHaveBeenCalled(); }); }); describe('window behaviour', () => { - it('when windows are dragged', () => { + it('when windows are dragged', async () => { + const user = userEvent.setup(); const mockDragStop = jest.fn(); - wrapper = createWrapper({ + const { container } = createWrapper({ layout, updateElasticWindowLayout: mockDragStop, }); - wrapper - .find(Rnd) - .props() - .onDragStop('myevent', { - x: 200, - y: 200, - }); + + const el = container.querySelector('.mirador-window-top-bar'); + + const coords = { + clientX: 200, + clientY: 200, + }; + + await user.pointer([ + { keys: '[MouseLeft>]', target: el }, + { coords, keys: '[/MouseLeft]', target: el }, + ]); + expect(mockDragStop).toHaveBeenCalledWith('1', { - x: -2300, - y: -2300, + x: 20 + 200, + y: 20 + 200, }); }); - it('when windows are resized', () => { + it('when windows are resized', async () => { + const user = userEvent.setup(); const mockOnResize = jest.fn(); - wrapper = createWrapper({ + const { container } = createWrapper({ layout, updateElasticWindowLayout: mockOnResize, }); - wrapper - .find(Rnd) - .props() - .onResize('myevent', 'direction', { - style: { - height: '200px', - width: '400px', - }, - }, {}, { x: 0, y: 0 }); - expect(mockOnResize).toHaveBeenCalledWith('1', { + + container.getBoundingClientRect = () => ({ + left: -2500, + offsetHeight: 5000, + offsetWidth: 5000, + top: -2500, + }); + + const el = container.querySelector('[style="position: absolute; user-select: none; width: 20px; height: 20px; right: -10px; bottom: -10px; cursor: se-resize;"]'); + + const oldCoords = { + x: 0, + y: 0, + }; + + const coords = { + x: 400, + y: 200, + }; + + await user.pointer([ + { coords: oldCoords, keys: '[MouseLeft>]', target: el }, + { coords }, + { coords, keys: '[/MouseLeft]', target: el }, + ]); + expect(mockOnResize).toHaveBeenCalledWith('1', expect.objectContaining({ height: 200, width: 400, - x: -2500, - y: -2500, - }); + })); }); }); }); diff --git a/__tests__/src/components/WorkspaceExport.test.js b/__tests__/src/components/WorkspaceExport.test.js index f8392f249f2302f52697c37320dd202b1454eb17..b4d5aa891330fbf17ed228a19f67acac32f225a0 100644 --- a/__tests__/src/components/WorkspaceExport.test.js +++ b/__tests__/src/components/WorkspaceExport.test.js @@ -1,16 +1,14 @@ -import { shallow } from 'enzyme'; -import Dialog from '@material-ui/core/Dialog'; -import Button from '@material-ui/core/Button'; -import Snackbar from '@material-ui/core/Snackbar'; -import { CopyToClipboard } from 'react-copy-to-clipboard'; +import { screen, render } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { WorkspaceExport } from '../../../src/components/WorkspaceExport'; describe('WorkspaceExport', () => { - let wrapper; + let user; let handleClose = jest.fn(); let mockState; beforeEach(() => { + user = userEvent.setup(); handleClose = jest.fn(); mockState = { companionWindows: {}, @@ -21,7 +19,7 @@ describe('WorkspaceExport', () => { workspace: {}, }; - wrapper = shallow( + render( <WorkspaceExport open handleClose={handleClose} @@ -31,38 +29,34 @@ describe('WorkspaceExport', () => { }); it('renders without an error', () => { - expect(wrapper.find(Dialog).length).toBe(1); + expect(screen.getByRole('dialog')).toBeInTheDocument(); }); it('renders sizing props', () => { - expect(wrapper.find(Dialog).props()).toEqual(expect.objectContaining({ - fullWidth: true, - maxWidth: 'sm', - })); + expect(screen.getByRole('dialog')).toHaveClass('MuiDialog-paperWidthSm'); }); - it('is closable by clicking the cancel button', () => { - expect(wrapper.find(Dialog).find(Button).at(0).text()).toMatch('cancel'); - wrapper.find(Dialog).find(Button).at(0).simulate('click'); + it('is closable by clicking the cancel button', async () => { + await user.click(screen.getByRole('button', { name: 'cancel' })); expect(handleClose).toHaveBeenCalled(); }); - it('reveals a snackbar on copy', () => { - expect(wrapper.find(Dialog).find(Button).at(1).text()).toMatch('copy'); - wrapper.find(Dialog).find(CopyToClipboard).simulate('copy'); - expect(wrapper.find(Snackbar).length).toBe(1); - shallow(wrapper.find(Snackbar).props().action).simulate('click'); + it('reveals a snackbar on copy', async () => { + // jsdom doesn't support the clipboard API or prompt (used as a fallback) + // so we mock the prompt at least to avoid a warning in the test output + jest.spyOn(window, 'prompt').mockImplementation(() => true); + + await user.click(screen.getByRole('button', { name: 'copy' })); + expect(screen.getByRole('alert')).toHaveTextContent('exportCopied'); + + await user.click(screen.getByRole('button', { name: 'dismiss' })); expect(handleClose).toHaveBeenCalled(); }); - it('renders an exportable version of state', () => { - expect(wrapper.find('pre').length).toBe(1); - expect(wrapper.find('pre').text()).toMatch('"companionWindows":'); - expect(wrapper.find('pre').text()).toMatch('"config":'); - expect(wrapper.find('pre').text()).toMatch('"elasticLayout":'); - expect(wrapper.find('pre').text()).toMatch('"viewers":'); - expect(wrapper.find('pre').text()).toMatch('"windows":'); - expect(wrapper.find('pre').text()).toMatch('"workspace":'); - expect(wrapper.find('pre').text()).not.toMatch('"manifests":'); + it('renders an exportable version of state', async () => { + await user.click(screen.getByRole('button', { name: 'viewWorkspaceConfiguration' })); + expect(screen.getByRole('region').querySelector('pre')).toHaveTextContent( // eslint-disable-line testing-library/no-node-access + '{ "companionWindows": {}, "config": {}, "elasticLayout": {}, "viewers": {}, "windows": {}, "workspace": {} }', + ); }); }); diff --git a/__tests__/src/components/WorkspaceImport.test.js b/__tests__/src/components/WorkspaceImport.test.js index 02b54e126d8d5ca16447fee9c2a39d9f8fab220b..fa9a1020d1e9dd67b7d613ac2ee5c1c2d9e904f0 100644 --- a/__tests__/src/components/WorkspaceImport.test.js +++ b/__tests__/src/components/WorkspaceImport.test.js @@ -1,40 +1,27 @@ -import { shallow } from 'enzyme'; -import Dialog from '@material-ui/core/Dialog'; -import TextField from '@material-ui/core/TextField'; +import { screen, render } from 'test-utils'; import { WorkspaceImport } from '../../../src/components/WorkspaceImport'; describe('WorkspaceImport', () => { - let wrapper; let handleClose; - let mockState; beforeEach(() => { handleClose = jest.fn(); - mockState = { - configImportValue: {}, - }; - wrapper = shallow( + render( <WorkspaceImport open handleClose={handleClose} - state={mockState} />, ); }); it('renders without an error', () => { - expect(wrapper.find(Dialog).length).toBe(1); + expect(screen.getByRole('dialog')).toBeInTheDocument(); }); it('renders sizing props', () => { - expect(wrapper.find(Dialog).props()).toEqual(expect.objectContaining({ - fullWidth: true, - maxWidth: 'sm', - })); + expect(screen.getByRole('dialog')).toHaveClass('MuiDialog-paperWidthSm'); }); it('renders TextField props', () => { - expect(wrapper.find(TextField).props()).toEqual(expect.objectContaining({ - inputProps: { autoFocus: 'autofocus' }, - })); + expect(screen.getByRole('textbox')).toHaveFocus(); }); }); diff --git a/__tests__/src/components/WorkspaceMenu.test.js b/__tests__/src/components/WorkspaceMenu.test.js index a8c56fe50ed894e226ca9906ef0f6ab2cae1083f..e701aacfa3ac53ce922df2d5cf6a11dcc48699f0 100644 --- a/__tests__/src/components/WorkspaceMenu.test.js +++ b/__tests__/src/components/WorkspaceMenu.test.js @@ -1,13 +1,15 @@ -import { shallow } from 'enzyme'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { WorkspaceMenu } from '../../../src/components/WorkspaceMenu'; /** */ function createShallow(props) { - return shallow( + render(<div data-testid="container" />); + + return render( <WorkspaceMenu - containerId="mirador" + anchorEl={screen.getByTestId('container')} + open showThemePicker {...props} />, @@ -15,7 +17,6 @@ function createShallow(props) { } describe('WorkspaceMenu', () => { - let wrapper; let handleClose; const showZoomControls = false; let toggleZoomControls; @@ -23,31 +24,37 @@ describe('WorkspaceMenu', () => { beforeEach(() => { handleClose = jest.fn(); toggleZoomControls = jest.fn(); - wrapper = createShallow({ handleClose, showZoomControls, toggleZoomControls }); }); it('renders without an error', () => { - expect(wrapper.find(Menu).length).toBe(1); + createShallow({ handleClose, showZoomControls, toggleZoomControls }); + expect(screen.getByRole('menu')).toBeInTheDocument(); }); - it('closes the current menu when opening a submenu', () => { - wrapper.find(MenuItem).first().simulate('click', {}); + it('closes the current menu when opening a submenu', async () => { + const user = userEvent.setup(); + createShallow({ handleClose, showZoomControls, toggleZoomControls }); + + await user.click(screen.getByRole('menuitem', { name: 'showZoomControls' })); expect(handleClose).toBeCalled(); }); it('disables zoom controls if the workspaceAdd UI is visible', () => { - expect(wrapper.find(MenuItem).at(0).props().disabled).toBe(false); - - wrapper = createShallow({ + createShallow({ handleClose, isWorkspaceAddVisible: true, showZoomControls, toggleZoomControls, }); - expect(wrapper.find(MenuItem).at(0).props().disabled).toBe(true); + expect(screen.getByRole('menuitem', { name: 'showZoomControls' })).toHaveAttribute('aria-disabled', 'true'); }); describe('handleZoomToggleClick', () => { - it('resets the anchor state', () => { - wrapper.instance().handleZoomToggleClick(); + it('resets the anchor state', async () => { + const user = userEvent.setup(); + + createShallow({ handleClose, showZoomControls, toggleZoomControls }); + + await user.click(screen.getByRole('menuitem', { name: 'showZoomControls' })); + expect(toggleZoomControls).toBeCalledWith(true); }); }); diff --git a/__tests__/src/components/WorkspaceMenuButton.test.js b/__tests__/src/components/WorkspaceMenuButton.test.js index 3e8a1a0fd65b600cf7090c3385baba9d711a0975..aca4b3d8df97442680c3008c5997694a914f4454 100644 --- a/__tests__/src/components/WorkspaceMenuButton.test.js +++ b/__tests__/src/components/WorkspaceMenuButton.test.js @@ -1,26 +1,29 @@ -import { shallow } from 'enzyme'; -import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; + import { WorkspaceMenuButton } from '../../../src/components/WorkspaceMenuButton'; describe('WorkspaceMenuButton', () => { - let wrapper; + let user; beforeEach(() => { - wrapper = shallow( + user = userEvent.setup(); + render( <WorkspaceMenuButton classes={{ ctrlBtnSelected: 'ctrlBtnSelected' }} />, ); }); - it('renders without an error', () => { - expect(wrapper.find(MiradorMenuButton).length).toBe(1); + it('renders the button', () => { + expect(screen.getByRole('button')).toHaveAccessibleName('workspaceMenu'); }); - it('the button has a class indicating that it is "selected" once it is clicked', () => { - const menuButton = wrapper.find(MiradorMenuButton).first(); + it('toggles open/close of <WorkspaceOptionsMenu /> when clicked', async () => { + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + + await user.click(screen.getByRole('button')); + expect(screen.getByRole('menu')).toBeInTheDocument(); - expect(wrapper.find(MiradorMenuButton).first().props().className).toEqual(''); - menuButton.props().onClick({ currentTarget: 'anElement' }); - expect(wrapper.find(MiradorMenuButton).first().props().className).toEqual('ctrlBtnSelected'); - menuButton.props().onClick({}); - expect(wrapper.find(MiradorMenuButton).first().props().className).toEqual(''); + // click something else to close the menu (the windowMenu button is hidden at this point) + await user.click(screen.getAllByRole('menuitem')[0]); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/WorkspaceMosaic.test.js b/__tests__/src/components/WorkspaceMosaic.test.js index 9fde83e6758fd34ffe2e0cb816b2e66ccbb22d7e..a77d63fb2ebb32a4f6ad29b70c393f13f2ff7a75 100644 --- a/__tests__/src/components/WorkspaceMosaic.test.js +++ b/__tests__/src/components/WorkspaceMosaic.test.js @@ -1,11 +1,12 @@ -import { shallow } from 'enzyme'; -import { MosaicWithoutDragDropContext } from 'react-mosaic-component'; -import MosaicRenderPreview from '../../../src/containers/MosaicRenderPreview'; +import { render, screen, fireEvent } from 'test-utils'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; + import { WorkspaceMosaic } from '../../../src/components/WorkspaceMosaic'; /** create wrapper */ function createWrapper(props) { - return shallow( + return render( <WorkspaceMosaic classes={{}} windowIds={[]} @@ -13,21 +14,35 @@ function createWrapper(props) { updateWorkspaceMosaicLayout={() => {}} {...props} />, + { + preloadedState: { + windows: { + 1: { companionWindowIds: [] }, + 2: { companionWindowIds: [] }, + 3: { companionWindowIds: [] }, + }, + }, + }, ); } +/* eslint-disable testing-library/no-node-access */ describe('WorkspaceMosaic', () => { const windowIds = ['1', '2']; let wrapper; - beforeEach(() => { - wrapper = createWrapper({ windowIds }); - }); it('should render properly with an initialValue', () => { - expect(wrapper.find(MosaicWithoutDragDropContext).length).toEqual(1); - expect(wrapper.find(MosaicWithoutDragDropContext).prop('initialValue')).toEqual({ - direction: 'row', first: '1', second: '2', + wrapper = createWrapper({ windowIds }); + const tiles = wrapper.container.querySelectorAll('.mosaic-tile'); + + expect(tiles).toHaveLength(2); + expect(tiles[0]).toHaveStyle({ + bottom: '0%', left: '0%', right: '50%', top: '0%', + }); + expect(tiles[1]).toHaveStyle({ + bottom: '0%', left: '50%', right: '0%', top: '0%', }); }); + describe('componentDidUpdate', () => { it('updates the workspace layout when windows change', () => { const updateWorkspaceMosaicLayout = jest.fn(); @@ -36,20 +51,28 @@ describe('WorkspaceMosaic', () => { windowIds, }); - wrapper.setProps({ windowIds: [...windowIds, '3'] }); + wrapper.rerender( + <WorkspaceMosaic classes={{}} windowIds={['1', '2', '3']} workspaceId="foo" updateWorkspaceMosaicLayout={updateWorkspaceMosaicLayout} />, + ); expect(updateWorkspaceMosaicLayout).toHaveBeenCalled(); }); it('updates the workspace layout when windows are removed', () => { const updateWorkspaceMosaicLayout = jest.fn(); - wrapper = createWrapper({ - layout: { first: 1, second: 2 }, + const props = { + classes: {}, + layout: { direction: 'row', first: '1', second: '2' }, updateWorkspaceMosaicLayout, windowIds, - }); - wrapper.instance().windowPaths = { 2: ['second'] }; - wrapper.setProps({ windowIds: [1] }); - expect(updateWorkspaceMosaicLayout).toHaveBeenLastCalledWith(1); + workspaceId: 'foo', + }; + wrapper = createWrapper(props); + + wrapper.rerender( + <WorkspaceMosaic {...props} windowIds={['1']} />, + ); + + expect(updateWorkspaceMosaicLayout).toHaveBeenLastCalledWith('1'); }); it('when no windows remain', () => { const updateWorkspaceMosaicLayout = jest.fn(); @@ -57,7 +80,10 @@ describe('WorkspaceMosaic', () => { updateWorkspaceMosaicLayout, windowIds, }); - wrapper.setProps({ windowIds: [] }); + + wrapper.rerender( + <WorkspaceMosaic classes={{}} windowIds={[]} workspaceId="foo" updateWorkspaceMosaicLayout={updateWorkspaceMosaicLayout} />, + ); expect(updateWorkspaceMosaicLayout).toHaveBeenLastCalledWith(null); }); it('when the new and old layouts are the same', () => { @@ -67,66 +93,63 @@ describe('WorkspaceMosaic', () => { updateWorkspaceMosaicLayout, windowIds, }); - wrapper.setProps({ layout: { first: 1, second: 2 }, windowIds }); + + wrapper.rerender( + <WorkspaceMosaic classes={{}} windowIds={windowIds} layout={{ first: 1, second: 2 }} workspaceId="foo" updateWorkspaceMosaicLayout={updateWorkspaceMosaicLayout} />, + ); + expect(updateWorkspaceMosaicLayout).toHaveBeenCalledTimes(1); }); }); - describe('bookkeepPath', () => { - it('as windows are rendered keeps a reference to their path in binary tree', () => { - wrapper.instance().tileRenderer('1', 'foo'); - expect(wrapper.instance().windowPaths).toEqual({ 1: 'foo' }); - }); - }); - describe('determineWorkspaceLayout', () => { - it('when window ids do not match workspace layout', () => { - wrapper = createWrapper({ layout: {}, windowIds }); - expect(wrapper.instance().determineWorkspaceLayout()).toMatchObject({ - direction: 'row', first: '1', second: '2', - }); - }); - it('by default use workspace.layout', () => { - wrapper = createWrapper({ layout: {}, windowIds: ['foo'] }); - expect(wrapper.instance().determineWorkspaceLayout()).toEqual('foo'); - }); - it('generates a new layout if windows do not match current layout', () => { - wrapper = createWrapper({ layout: { first: 'foo', second: 'bark' }, windowIds: ['foo'] }); - expect(wrapper.instance().determineWorkspaceLayout()).toEqual('foo'); - }); - it('when window ids match workspace layout', () => { - wrapper = createWrapper({ layout: {}, windowIds: ['foo'] }); - expect(wrapper.instance().determineWorkspaceLayout()).toBe('foo'); - }); - }); - describe('tileRenderer', () => { + describe('tile rendering', () => { it('when window is available', () => { - const renderedTile = wrapper.instance().tileRenderer('1', 'foo'); - expect(renderedTile).not.toBeNull(); - expect(shallow(renderedTile).find('DropTarget(DragSource(InternalMosaicWindow))').length).toEqual(1); - expect(shallow(renderedTile).props()).toEqual(expect.objectContaining({ - additionalControls: [], - path: 'foo', - toolbarControls: [], - })); - - expect(shallow(shallow(renderedTile).props().renderPreview({ windowId: 1 })).matchesElement( - <div className="mosaic-preview" aria-hidden> - <MosaicRenderPreview windowId={1} /> - </div>, - )).toBe(true); - }); - it('when window is not available', () => { - expect(wrapper.instance().tileRenderer('bar')).toBeNull(); + wrapper = createWrapper({ windowIds }); + + expect(screen.getAllByLabelText('window', { container: 'section' })[0]).toHaveAttribute('id', '1'); + expect(screen.getAllByLabelText('window', { container: 'section' })[1]).toHaveAttribute('id', '2'); + + expect(wrapper.container.querySelector('.mosaic-window-title')).toBeEmptyDOMElement(); + expect(wrapper.container.querySelector('.mosaic-window-controls')).toBeEmptyDOMElement(); + expect(wrapper.container.querySelectorAll('.mosaic-preview')).toHaveLength(2); + expect(wrapper.container.querySelector('.mosaic-preview')).toHaveAttribute('aria-hidden', 'true'); }); }); describe('mosaicChange', () => { - it('calls the provided prop to update layout', () => { + it('calls the provided prop to update layout', async () => { const updateWorkspaceMosaicLayout = jest.fn(); - wrapper = createWrapper({ - updateWorkspaceMosaicLayout, - windowIds, - }); - wrapper.instance().mosaicChange(); + const { container } = render( + <DndProvider backend={HTML5Backend}> + <WorkspaceMosaic + classes={{}} + windowIds={['1', '2']} + workspaceId="foo" + updateWorkspaceMosaicLayout={updateWorkspaceMosaicLayout} + /> + </DndProvider>, + { + preloadedState: { + windows: { + 1: { companionWindowIds: [], maximized: false }, + 2: { companionWindowIds: [], maximized: false }, + }, + workspace: { + type: 'mosaic', + windowIds: ['1', '2'], + }, + }, + }, + ); + + const dragTarget = screen.getAllByLabelText('windowNavigation')[0]; + const dropTarget = container.querySelector('.mirador-mosaic > .drop-target-container > .drop-target.top'); // eslint-disable-line testing-library/no-container + + fireEvent.dragStart(dragTarget); + fireEvent.drag(dragTarget); + fireEvent.dragEnter(dropTarget); + fireEvent.dragOver(dropTarget); + fireEvent.drop(dropTarget); + expect(updateWorkspaceMosaicLayout).toBeCalled(); }); }); diff --git a/__tests__/src/components/WorkspaceOptionsButton.test.js b/__tests__/src/components/WorkspaceOptionsButton.test.js index d9b20cd029998c4f068488bbef529b06b843a867..c70f1a8d98ecd4135c4f83a752fea0c152dbe797 100644 --- a/__tests__/src/components/WorkspaceOptionsButton.test.js +++ b/__tests__/src/components/WorkspaceOptionsButton.test.js @@ -1,44 +1,42 @@ -import { shallow } from 'enzyme'; -import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; -import WorkspaceOptionsMenu from '../../../src/containers/WorkspaceOptionsMenu'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { WorkspaceOptionsButton } from '../../../src/components/WorkspaceOptionsButton'; -/** Utility helper to create a shallow wrapper around WorkspaceOptionsButton */ -function createShallow(props) { - return shallow( - <WorkspaceOptionsButton - classes={{}} - t={k => k} - {...props} - />, +/** create wrapper */ +function Subject({ ...props }) { + return ( + <div> + <WorkspaceOptionsButton + classes={{}} + t={k => k} + {...props} + /> + , + , + </div> ); } describe('WorkspaceOptionsButton', () => { - let wrapper; - - it('renders a button and the menu', () => { - wrapper = createShallow(); - - expect(wrapper.find(MiradorMenuButton).length).toEqual(1); - expect(wrapper.find(WorkspaceOptionsMenu).length).toEqual(1); + let user; + beforeEach(() => { + user = userEvent.setup(); }); - it('sets the anchorEl state (and passes that to the menu) on button click', () => { - wrapper = createShallow(); - - expect(wrapper.state().anchorEl).toBeNull(); - wrapper.find(MiradorMenuButton).simulate('click', { currentTarget: { id: 'blah' } }); - expect(wrapper.state().anchorEl).toEqual({ id: 'blah' }); + it('renders the button', () => { + render(<Subject />); + expect(screen.getByLabelText('workspaceOptions')).toBeInTheDocument(); }); - it('sends a handleClose prop to the WorkspaceOptionsMenu that clears the anchorEl', () => { - wrapper = createShallow(); + it('toggles open/close of <WorkspaceOptionsMenu /> when clicked', async () => { + render(<Subject />); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); - wrapper.setState({ anchorEl: { id: 'blah' } }); - expect(wrapper.state().anchorEl).toEqual({ id: 'blah' }); + await user.click(screen.getByLabelText('workspaceOptions')); + expect(screen.getByRole('menu')).toBeInTheDocument(); - wrapper.find(WorkspaceOptionsMenu).props().handleClose(); - expect(wrapper.state().anchorEl).toBeNull(); + // click something else to close the menu (the windowMenu button is hidden at this point) + await user.click(screen.getAllByRole('menuitem')[0]); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); }); }); diff --git a/__tests__/src/components/WorkspaceOptionsMenu.test.js b/__tests__/src/components/WorkspaceOptionsMenu.test.js index 5f60cbf35b550b44b7a1c9be45668c8aa282a02b..308dc459c11212ffe9983f1771f70655bb6ced90 100644 --- a/__tests__/src/components/WorkspaceOptionsMenu.test.js +++ b/__tests__/src/components/WorkspaceOptionsMenu.test.js @@ -1,41 +1,75 @@ -import { shallow } from 'enzyme'; -import MenuItem from '@material-ui/core/MenuItem'; -import WorkspaceExport from '../../../src/containers/WorkspaceExport'; -import WorkspaceImport from '../../../src/containers/WorkspaceImport'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { WorkspaceOptionsMenu } from '../../../src/components/WorkspaceOptionsMenu'; -/** Utility helper to create a shallow wrapper around WorkspaceOptionsButton */ -function createShallow(props) { - return shallow( - <WorkspaceOptionsMenu - containerId="mirador" - handleClose={() => {}} - t={k => k} - {...props} - />, +/** create wrapper */ +function Subject({ ...props }) { + return ( + <div> + <WorkspaceOptionsMenu + handleClose={() => {}} + t={k => k} + {...props} + /> + , + , + </div> + ); +} + +/** create anchor element */ +function createAnchor() { + return render( + <button type="button" data-testid="menu-trigger-button">Button</button>, ); } describe('WorkspaceOptionsMenu', () => { - let wrapper; + let user; + beforeEach(() => { + createAnchor(); + user = userEvent.setup(); + }); - it('toggles the relevant section with each MenuItem click', () => { - wrapper = createShallow(); + it('renders all needed elements when open', () => { + render(<Subject anchorEl={screen.getByTestId('menu-trigger-button')} open />); - expect(wrapper.find(MenuItem).length).toEqual(2); - expect(wrapper.find(WorkspaceExport).length).toEqual(0); - expect(wrapper.find(WorkspaceImport).length).toEqual(0); + expect(screen.getByRole('menu')).toBeInTheDocument(); - wrapper.find(MenuItem).at(0).simulate('click'); - expect(wrapper.find(WorkspaceExport).length).toEqual(1); - wrapper.find(MenuItem).at(1).simulate('click'); - expect(wrapper.find(WorkspaceImport).length).toEqual(1); + const menuItems = screen.getAllByRole('menuitem'); + expect(menuItems).toHaveLength(2); + expect(menuItems[0]).toHaveTextContent('downloadExportWorkspace'); + expect(menuItems[1]).toHaveTextContent('importWorkspace'); }); - it('it passes a handleClose prop to the various components to that closes that will close the component', () => { - wrapper.setState({ exportWorkspace: { open: true } }); - expect(wrapper.state().exportWorkspace.open).toBe(true); - wrapper.find(WorkspaceExport).props().handleClose(); - expect(wrapper.state().exportWorkspace.open).toBe(false); + it('does not display unless open', () => { + render(<Subject open={false} />); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + }); + + it('renders the export dialog when export option is clicked', async () => { + render(<Subject anchorEl={screen.getByTestId('menu-trigger-button')} open />); + expect(document.querySelector('#workspace-export')).not.toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access + + await user.click(screen.getAllByRole('menuitem')[0]); + expect(document.querySelector('#workspace-export')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access + }); + + it('renders the import dialog when imporrt option is clicked', async () => { + render(<Subject anchorEl={screen.getByTestId('menu-trigger-button')} open />); + expect(document.querySelector('#workspace-import')).not.toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access + + await user.click(screen.getAllByRole('menuitem')[1]); + expect(document.querySelector('#workspace-import')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access + }); + + it('fires the correct callbacks on menu close', async () => { + const handleClose = jest.fn(); + render(<Subject anchorEl={screen.getByTestId('menu-trigger-button')} handleClose={handleClose} open />); + + // click a menu item should close the menu + const menuItems = screen.getAllByRole('menuitem'); + await user.click(menuItems[0]); + expect(handleClose).toHaveBeenCalledTimes(1); }); }); diff --git a/__tests__/src/components/WorkspaceSelectionDialog.test.js b/__tests__/src/components/WorkspaceSelectionDialog.test.js index 895a4a68f9114fb2b80d4b547869621f628ee54c..23d7a841c6cfdf3824ff9e20acd97495fe9b869e 100644 --- a/__tests__/src/components/WorkspaceSelectionDialog.test.js +++ b/__tests__/src/components/WorkspaceSelectionDialog.test.js @@ -1,10 +1,10 @@ -import { shallow } from 'enzyme'; -import Dialog from '@material-ui/core/Dialog'; -import MenuItem from '@material-ui/core/MenuItem'; +import { + render, screen, waitFor, +} from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { WorkspaceSelectionDialog } from '../../../src/components/WorkspaceSelectionDialog'; -describe('WorkspaceSettings', () => { - let wrapper; +describe('WorkspaceSelectionDialog', () => { let handleClose; let updateWorkspace; @@ -16,9 +16,9 @@ describe('WorkspaceSettings', () => { handleClose = jest.fn(); updateWorkspace = jest.fn(); - return shallow( + return render( <WorkspaceSelectionDialog - classes={{ list: {} }} + classes={{ list: 'list' }} open handleClose={handleClose} updateWorkspace={updateWorkspace} @@ -29,34 +29,31 @@ describe('WorkspaceSettings', () => { } it('renders without an error', () => { - wrapper = createWrapper(); - expect(wrapper.matchesElement(WorkspaceSelectionDialog)); + createWrapper(); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: /elastic/ })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: /mosaic/ })).toBeInTheDocument(); }); - it('sends the updateConfig and handleClose props on workspace selection', () => { - wrapper = createWrapper(); + it('sends the updateConfig and handleClose props on workspace selection', async () => { + const user = userEvent.setup(); + createWrapper(); + + user.click(screen.getByRole('menuitem', { name: /elastic/ })); + await waitFor(() => expect(updateWorkspace).toHaveBeenLastCalledWith({ type: 'elastic' })); - wrapper.find(MenuItem).at(0).simulate('click'); - expect(updateWorkspace).toHaveBeenLastCalledWith({ type: 'elastic' }); - wrapper.find(MenuItem).at(1).simulate('click'); - expect(updateWorkspace).toHaveBeenLastCalledWith({ type: 'mosaic' }); - expect(handleClose).toHaveBeenCalledTimes(2); + user.click(screen.getByRole('menuitem', { name: /mosaic/ })); + await waitFor(() => expect(updateWorkspace).toHaveBeenLastCalledWith({ type: 'mosaic' })); + await waitFor(() => expect(handleClose).toHaveBeenCalledTimes(2)); }); describe('inital focus', () => { - const mockMenuItemFocus = jest.fn(); - const mockMenu = { - querySelectorAll: (selector) => { - expect(selector).toEqual('li[value="elastic"]'); - return [{ focus: mockMenuItemFocus }]; - }, - }; - it('sets an onEntered prop on the Dialog that focuses the selected item', () => { - wrapper = createWrapper(); + createWrapper(); - wrapper.find(Dialog).props().onEntered(mockMenu); - expect(mockMenuItemFocus).toHaveBeenCalled(); + const menuItem = screen.getByRole('menuitem', { name: /elastic/ }); + expect(menuItem).toHaveFocus(); }); }); }); diff --git a/__tests__/src/components/ZoomControls.test.js b/__tests__/src/components/ZoomControls.test.js index e7e49671be7bf281fc6cf961e340a3d669f889ee..41a562513eb8b5d87d6765b003ebbc9771046786 100644 --- a/__tests__/src/components/ZoomControls.test.js +++ b/__tests__/src/components/ZoomControls.test.js @@ -1,89 +1,52 @@ -import { shallow } from 'enzyme'; -import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; +import { render, screen } from 'test-utils'; +import userEvent from '@testing-library/user-event'; import { ZoomControls } from '../../../src/components/ZoomControls'; /** Utility function to create a shallow rendering */ function createWrapper(props) { - return shallow( + return render( <ZoomControls - classes={{ divider: 'divider', zoom_controls: 'zoom_controls' }} windowId="xyz" zoomToWorld={() => {}} {...props} - />, ); } describe('ZoomControls', () => { - let wrapper; const viewer = { x: 100, y: 100, zoom: 1 }; - const showZoomControls = false; let updateViewport; + const zoomToWorld = jest.fn(); + let user; beforeEach(() => { + user = userEvent.setup(); updateViewport = jest.fn(); - wrapper = createWrapper({ showZoomControls, updateViewport, viewer }); - }); - - describe('with showZoomControls=false', () => { - it('renders nothing unless asked', () => { - expect(wrapper.find('div.zoom_controls').length).toBe(0); + createWrapper({ + updateViewport, viewer, zoomToWorld, }); }); - describe('with showZoomControls=true', () => { - const zoomToWorld = jest.fn(); - beforeEach(() => { - updateViewport = jest.fn(); - wrapper = createWrapper({ - showZoomControls: true, updateViewport, viewer, zoomToWorld, - }); - }); - - it('renders a couple buttons', () => { - expect(wrapper.find('div.zoom_controls').length).toBe(1); - expect(wrapper.find(MiradorMenuButton).length).toBe(3); - }); - - it('has a zoom-in button', () => { - const button = wrapper.find({ 'aria-label': 'zoomIn' }).first(); - button.props().onClick(); // Trigger the onClick prop - expect(updateViewport).toHaveBeenCalledTimes(1); - expect(updateViewport).toHaveBeenCalledWith('xyz', { zoom: 2 }); - }); + it('renders a couple buttons', () => { + expect(screen.getByRole('button', { name: 'zoomIn' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'zoomOut' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'zoomReset' })).toBeInTheDocument(); + }); - it('has a zoom-out button', () => { - const button = wrapper.find({ 'aria-label': 'zoomOut' }).first(); - button.props().onClick(); // Trigger the onClick prop - expect(updateViewport).toHaveBeenCalledTimes(1); - expect(updateViewport).toHaveBeenCalledWith('xyz', { zoom: 0.5 }); - }); + it('has a zoom-in button', async () => { + await user.click(screen.getByRole('button', { name: 'zoomIn' })); - it('has a zoom reset button', () => { - const button = wrapper.find({ 'aria-label': 'zoomReset' }).first(); - button.props().onClick(); // Trigger the onClick prop - expect(zoomToWorld).toHaveBeenCalledTimes(1); - expect(zoomToWorld).toHaveBeenCalledWith(false); - }); + expect(updateViewport).toHaveBeenCalledWith('xyz', { zoom: 2 }); }); - describe('handleZoomInClick', () => { - it('increases the zoom value on Zoom-In', () => { - wrapper.instance().handleZoomInClick(); - expect(updateViewport).toHaveBeenCalled(); - }); + it('has a zoom-out button', async () => { + await user.click(screen.getByRole('button', { name: 'zoomOut' })); + expect(updateViewport).toHaveBeenCalledWith('xyz', { zoom: 0.5 }); }); - describe('responsive divider', () => { - it('is present when the displayDivider prop is true (default)', () => { - wrapper = createWrapper({ showZoomControls: true, viewer }); - expect(wrapper.find('span.divider').length).toEqual(1); - }); + it('has a zoom reset button', async () => { + await user.click(screen.getByRole('button', { name: 'zoomReset' })); - it('is not present when the displayDivider prop is false', () => { - wrapper = createWrapper({ displayDivider: false, showZoomControls: true, viewer }); - expect(wrapper.find('.divider').length).toEqual(0); - }); + expect(zoomToWorld).toHaveBeenCalledWith(false); }); }); diff --git a/__tests__/src/extend/withPlugins.test.js b/__tests__/src/extend/withPlugins.test.js index cbf62333402770850676c4b6fc448231337af9be..524db2a4d78a9fc474e8b24b6341b939983e1da2 100644 --- a/__tests__/src/extend/withPlugins.test.js +++ b/__tests__/src/extend/withPlugins.test.js @@ -1,16 +1,27 @@ import PropTypes from 'prop-types'; -import { mount } from 'enzyme'; +import { render, screen, within } from 'test-utils'; import { withPlugins } from '../../../src/extend/withPlugins'; +import { PluginHook } from '../../../src/components/PluginHook'; import PluginContext from '../../../src/extend/PluginContext'; /** Mock target component */ -const Target = props => <div>Hello</div>; +const Target = ({ PluginComponents, TargetComponent, ...props }) => ( + <div data-testid="target" {...props}> + Hello + <PluginHook PluginComponents={PluginComponents} {...props} /> + </div> +); + +Target.propTypes = { + PluginComponents: PropTypes.arrayOf(PropTypes.element).isRequired, + TargetComponent: PropTypes.elementType.isRequired, +}; /** create wrapper */ function createPluginHoc(pluginMap) { const props = { bar: 2, foo: 1 }; const PluginHoc = withPlugins('Target', Target); - return mount( + return render( <PluginContext.Provider value={pluginMap}> <PluginHoc {...props} /> </PluginContext.Provider>, @@ -34,36 +45,33 @@ describe('withPlugins', () => { describe('PluginHoc: if no plugin exists for the target', () => { it('renders the target component', () => { - const hoc = createPluginHoc({}); - expect(hoc.find(Target).length).toBe(1); - expect(hoc.find(Target).props().foo).toBe(1); - expect(hoc.find(Target).props().bar).toBe(2); + createPluginHoc({}); + + expect(screen.getByTestId('target')).toHaveAttribute('foo', '1'); + expect(screen.getByTestId('target')).toHaveAttribute('bar', '2'); }); }); describe('PluginHoc: if wrap plugins exist for target', () => { - it('renders the first wrap plugin and passes the target component and the target props to it', () => { - /** */ const WrapPluginComponentA = props => <div>look i am a plugin</div>; - /** */ const WrapPluginComponentB = props => <div>look i am a plugin</div>; + it('renders the first wrap plugin', () => { + /** */ const WrapPluginComponentA = props => <div data-testid="plugin">look i am a plugin</div>; const pluginMap = { Target: { wrap: [ { component: WrapPluginComponentA, mode: 'wrap', target: 'Target' }, - { component: WrapPluginComponentB, mode: 'wrap', target: 'Target' }, ], }, }; - const hoc = createPluginHoc(pluginMap); - const selector = 'WrapPluginComponentA'; - expect(hoc.find(selector).length).toBe(1); - expect(hoc.find(selector).props().TargetComponent).toBe(Target); - expect(hoc.find(selector).props().targetProps).toEqual({ bar: 2, foo: 1 }); + createPluginHoc(pluginMap); + + expect(screen.getByTestId('plugin')).toBeInTheDocument(); + expect(screen.queryByTestId('target')).not.toBeInTheDocument(); }); it('passes the whole wrapped stack to the plugins', () => { - /** */ const WrapPluginComponentA = ({ children }) => <div className="pluginA">{children}</div>; + /** */ const WrapPluginComponentA = ({ children }) => <div data-testid="pluginA">{children}</div>; WrapPluginComponentA.propTypes = { children: PropTypes.node.isRequired }; - /** */ const WrapPluginComponentB = props => <div className="pluginB">look i am a plugin</div>; + /** */ const WrapPluginComponentB = props => <div data-testid="pluginB">look i am a plugin</div>; const pluginMap = { Target: { wrap: [ @@ -72,17 +80,17 @@ describe('PluginHoc: if wrap plugins exist for target', () => { ], }, }; - const hoc = createPluginHoc(pluginMap); - const selector = 'WrapPluginComponentA'; - expect(hoc.find(selector).length).toBe(1); - expect(hoc.find(selector).find('WrapPluginComponentB').length).toBe(1); + createPluginHoc(pluginMap); + + expect(screen.getByTestId('pluginA')).toBeInTheDocument(); + expect(within(screen.getByTestId('pluginA')).getByTestId('pluginB')).toBeInTheDocument(); }); }); describe('PluginHoc: if add plugins exist but no wrap plugin', () => { it('renders the target component and passes all add plugin components as a prop', () => { - /** */ const AddPluginComponentA = props => <div>look i am a plugin</div>; - /** */ const AddPluginComponentB = props => <div>look i am a plugin</div>; + /** */ const AddPluginComponentA = props => <div data-testid="a">look i am a plugin</div>; + /** */ const AddPluginComponentB = props => <div data-testid="b">look i am a plugin</div>; const plugins = { Target: { add: [ @@ -91,19 +99,18 @@ describe('PluginHoc: if add plugins exist but no wrap plugin', () => { ], }, }; - const hoc = createPluginHoc(plugins); - const selector = Target; - expect(hoc.find(selector).length).toBe(1); - expect(hoc.find(selector).props().foo).toBe(1); - expect(hoc.find(selector).props().bar).toBe(2); - expect(hoc.find(selector).props().PluginComponents[0]).toBe(AddPluginComponentA); - expect(hoc.find(selector).props().PluginComponents[1]).toBe(AddPluginComponentB); + + createPluginHoc(plugins); + + expect(screen.getByTestId('target')).toBeInTheDocument(); + expect(within(screen.getByTestId('target')).getByTestId('a')).toBeInTheDocument(); + expect(within(screen.getByTestId('target')).getByTestId('b')).toBeInTheDocument(); }); }); describe('PluginHoc: if wrap plugins AND add plugins exist for target', () => { it('renders the first wrap plugin, ignores add plugins if props are not passed through', () => { - /** */ const WrapPluginComponentA = props => <div>look i am a plugin</div>; + /** */ const WrapPluginComponentA = props => <div data-testid="a">look i am a plugin</div>; /** */ const WrapPluginComponentB = props => <div>look i am a plugin</div>; /** */ const AddPluginComponentA = props => <div>look i am a plugin</div>; /** */ const AddPluginComponentB = props => <div>look i am a plugin</div>; @@ -119,23 +126,32 @@ describe('PluginHoc: if wrap plugins AND add plugins exist for target', () => { ], }, }; - const hoc = createPluginHoc(plugins); - expect(hoc.find(WrapPluginComponentA).length).toBe(1); - expect(hoc.find(Target).length).toBe(0); + + createPluginHoc(plugins); + + expect(screen.getByTestId('a')).toBeInTheDocument(); + expect(screen.queryByTestId('target')).not.toBeInTheDocument(); }); it('renders the first wrap plugin, renders add plugins if plugin/props are passed through', () => { - /** */ const WrapPluginComponentA = plugin => ( + /** */ const WrapPluginComponentA = ({ targetProps, ...plugin }) => ( // eslint-disable-next-line react/destructuring-assignment - <plugin.TargetComponent {...plugin.targetProps} {...plugin} /> + <div data-testid="a"> + <plugin.TargetComponent {...targetProps} {...plugin} /> + </div> ); + + WrapPluginComponentA.propTypes = { + targetProps: PropTypes.shape({}).isRequired, + }; + /** */ const WrapPluginComponentB = props => <div>look i am a plugin</div>; - /** */ const AddPluginComponentA = props => <div>look i am a plugin</div>; - /** */ const AddPluginComponentB = props => <div>look i am a plugin</div>; + /** */ const AddPluginComponentC = props => <div data-testid="c">look i am a plugin</div>; + /** */ const AddPluginComponentD = props => <div data-testid="d">look i am a plugin</div>; const plugins = { Target: { add: [ - { component: AddPluginComponentA, mode: 'add', target: 'Target' }, - { component: AddPluginComponentB, mode: 'add', target: 'Target' }, + { component: AddPluginComponentC, mode: 'add', target: 'Target' }, + { component: AddPluginComponentD, mode: 'add', target: 'Target' }, ], wrap: [ { component: WrapPluginComponentA, mode: 'wrap', target: 'Target' }, @@ -143,8 +159,12 @@ describe('PluginHoc: if wrap plugins AND add plugins exist for target', () => { ], }, }; - const hoc = createPluginHoc(plugins); - expect(hoc.find(WrapPluginComponentA).length).toBe(1); - expect(hoc.find(Target).length).toBe(1); + + createPluginHoc(plugins); + + expect(screen.getByTestId('a')).toBeInTheDocument(); + expect(within(screen.getByTestId('a')).getByTestId('target')).toBeInTheDocument(); + expect(within(screen.getByTestId('target')).getByTestId('c')).toBeInTheDocument(); + expect(within(screen.getByTestId('target')).getByTestId('d')).toBeInTheDocument(); }); }); diff --git a/__tests__/src/lib/MiradorCanvas.test.js b/__tests__/src/lib/MiradorCanvas.test.js index 0a41c260c327c832beb69823429cffc2a6e487eb..5502c67746c13b725317588e3f411c695f326c35 100644 --- a/__tests__/src/lib/MiradorCanvas.test.js +++ b/__tests__/src/lib/MiradorCanvas.test.js @@ -8,6 +8,7 @@ import fragmentFixture from '../../fixtures/version-2/hamilton.json'; import fragmentFixtureV3 from '../../fixtures/version-3/hamilton.json'; import audioFixture from '../../fixtures/version-3/0002-mvm-audio.json'; import videoFixture from '../../fixtures/version-3/0015-start.json'; +import videoWithAnnoCaptions from '../../fixtures/version-3/video_with_annotation_captions.json'; describe('MiradorCanvas', () => { let instance; @@ -119,11 +120,17 @@ describe('MiradorCanvas', () => { }); }); describe('vttContent', () => { - it('returns vttContent', () => { + it('returns v2 vttContent', () => { instance = new MiradorCanvas( Utils.parseManifest(videoFixture).getSequences()[0].getCanvases()[0], ); - expect(instance.vttContent.length).toEqual(1); + expect(instance.v2VttContent.length).toEqual(1); + }); + it('returns v3 vttContent', () => { + instance = new MiradorCanvas( + Utils.parseManifest(videoWithAnnoCaptions).getSequences()[0].getCanvases()[0], + ); + expect(instance.v3VttContent.length).toEqual(1); }); }); }); diff --git a/__tests__/src/lib/MiradorViewer.test.js b/__tests__/src/lib/MiradorViewer.test.js index 11d7841ac4ca3d31d8595112663edbad184c8d7b..d5e22093d19936c3e0a943eabb3e917ff2a60957 100644 --- a/__tests__/src/lib/MiradorViewer.test.js +++ b/__tests__/src/lib/MiradorViewer.test.js @@ -1,35 +1,36 @@ -import ReactDOM from 'react-dom'; -import { shallow } from 'enzyme'; +import { render, screen } from 'test-utils'; import MiradorViewer from '../../../src/lib/MiradorViewer'; jest.unmock('react-i18next'); -jest.mock('react-dom'); + +/** */ +const DummyPlugin = () => <div data-testid="plugin">Plugin</div>; describe('MiradorViewer', () => { let container; - let instance; - beforeAll(() => { + beforeEach(() => { container = document.createElement('div'); container.id = 'mirador'; + container.dataset.testid = 'container'; document.body.appendChild(container); - ReactDOM.render = jest.fn(); - ReactDOM.unmountComponentAtNode = jest.fn(); - instance = new MiradorViewer({ id: 'mirador' }); }); - afterAll(() => { + afterEach(() => { document.body.removeChild(container); }); describe('constructor', () => { it('returns viewer store', () => { + const instance = new MiradorViewer({ id: 'mirador' }); expect(instance.store.dispatch).toBeDefined(); }); it('renders via ReactDOM', () => { - expect(ReactDOM.render).toHaveBeenCalled(); + const instance = new MiradorViewer({ id: 'mirador' }); // eslint-disable-line no-unused-vars + + expect(screen.getByTestId('container')).not.toBeEmptyDOMElement(); }); }); describe('processConfig', () => { it('transforms config values to actions to dispatch to store', () => { - instance = new MiradorViewer( + const instance = new MiradorViewer( { catalog: [ { manifestId: 'http://media.nga.gov/public/manifests/nga_highlights.json', provider: 'National Gallery of Art' }, @@ -49,6 +50,7 @@ describe('MiradorViewer', () => { }, { plugins: [{ + component: DummyPlugin, config: { foo: 'bar', }, @@ -75,13 +77,14 @@ describe('MiradorViewer', () => { expect(config.foo).toBe('bar'); }); it('merges translation configs from multiple plugins', () => { - instance = new MiradorViewer( + const instance = new MiradorViewer( { id: 'mirador', }, { plugins: [ { + component: DummyPlugin, config: { translations: { en: { @@ -93,6 +96,7 @@ describe('MiradorViewer', () => { target: 'WindowTopBarPluginArea', }, { + component: DummyPlugin, config: { translations: { en: { @@ -117,17 +121,26 @@ describe('MiradorViewer', () => { }); describe('render', () => { - it('passes props through to the App component', () => { - const rendered = shallow(instance.render({ some: 'prop' })); - expect(rendered.find('App').length).toBe(1); - expect(rendered.find('App').prop('some')).toBe('prop'); + it('passes props through to the App component', async () => { + const instance = new MiradorViewer({}); + const plugins = [{ + component: DummyPlugin, + mode: 'wrap', + target: 'WorkspaceArea', + }]; + + render(instance.render({ plugins })); + + expect(await screen.findByTestId('plugin')).toBeInTheDocument(); }); }); describe('unmount', () => { it('unmounts via ReactDOM', () => { + const instance = new MiradorViewer({ id: 'mirador' }); + expect(screen.getByTestId('container')).not.toBeEmptyDOMElement(); instance.unmount(); - expect(ReactDOM.unmountComponentAtNode).toHaveBeenCalled(); + expect(screen.getByTestId('container')).toBeEmptyDOMElement(); }); }); }); diff --git a/__tests__/src/selectors/canvases.test.js b/__tests__/src/selectors/canvases.test.js index 43d1a0f735df6545bf35ed622274b99498ea1483..47db6784f280ea5b0d41337d5439e959ec0d7bba 100644 --- a/__tests__/src/selectors/canvases.test.js +++ b/__tests__/src/selectors/canvases.test.js @@ -4,6 +4,7 @@ import minimumRequired from '../../fixtures/version-2/minimumRequired.json'; import minimumRequired3 from '../../fixtures/version-3/minimumRequired.json'; import audioFixture from '../../fixtures/version-3/0002-mvm-audio.json'; import videoFixture from '../../fixtures/version-3/0015-start.json'; +import videoWithAnnoCaptions from '../../fixtures/version-3/video_with_annotation_captions.json'; import settings from '../../../src/config/settings'; import { @@ -400,7 +401,7 @@ describe('getVisibleCanvasNonTiledResources', () => { }); describe('getVisibleCanvasCaptions', () => { - it('returns canvases resources', () => { + it('returns v2 canvases resources', () => { const state = { manifests: { 'https://iiif.io/api/cookbook/recipe/0015-start/manifest.json': { @@ -419,6 +420,25 @@ describe('getVisibleCanvasNonTiledResources', () => { }; expect(getVisibleCanvasCaptions(state, { windowId: 'a' })[0].id).toBe('https://example.com/file.vtt'); }); + it('returns v3 canvases resources', () => { + const state = { + manifests: { + 'https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/manifest.json': { + id: 'https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/manifest.json', + json: videoWithAnnoCaptions, + }, + }, + windows: { + b: { + manifestId: 'https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/manifest.json', + visibleCanvases: [ + 'https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/canvas', + ], + }, + }, + }; + expect(getVisibleCanvasCaptions(state, { windowId: 'b' })[0].id).toBe('https://fixtures.iiif.io/video/indiana/lunchroom_manners/lunchroom_manners.vtt'); + }); }); describe('getVisibleCanvasAudioResources', () => { diff --git a/__tests__/src/selectors/manifests.test.js b/__tests__/src/selectors/manifests.test.js index 28b0461ad54b9d46f89e5b30f2f8751134de346e..e760427ef1180b9052a1cf31d711a503aa4f369b 100644 --- a/__tests__/src/selectors/manifests.test.js +++ b/__tests__/src/selectors/manifests.test.js @@ -15,7 +15,8 @@ import { getManifestLogo, getManifestDescription, getManifestHomepage, - getManifestProvider, + getProviderLogo, + getManifestProviderName, getManifestTitle, getManifestThumbnail, getManifestMetadata, @@ -23,6 +24,7 @@ import { getManifestRelatedContent, getManifestRenderings, getManifestSeeAlso, + getManifestSummary, getManifestUrl, getMetadataLocales, getRequiredStatement, @@ -145,15 +147,43 @@ describe('getManifestDescription', () => { }); }); -describe('getManifestProvider', () => { +describe('getManifestSummary', () => { + it('should return manifest summary', () => { + const state = { manifests: { x: { json: manifestFixturev3001 } } }; + const received = getManifestSummary(state, { manifestId: 'x' }); + expect(received).toBe('[Handbill of Mr. Becket, [1787] ]'); + }); + + it('should return undefined if manifest undefined', () => { + const received = getManifestSummary({ manifests: {} }, { manifestId: 'x' }); + expect(received).toBeUndefined(); + }); +}); + +describe('getProviderLogo', () => { + it('should return manifest provider logo', () => { + const state = { manifests: { x: { json: manifestFixtureWithAProvider } } }; + const received = getProviderLogo(state, { manifestId: 'x' }); + expect(received).toBe('https://example.org/images/logo.png'); + }); + + it('should return null if no logo', () => { + // use the fixture but overwrite the 'provider' property to be empty/not include logo + const state = { manifests: { x: { json: { ...manifestFixtureWithAProvider, provider: [] } } } }; + const received = getProviderLogo(state, { manifestId: 'x' }); + expect(received).toBeNull(); + }); +}); + +describe('getManifestProviderName', () => { it('should return manifest provider label', () => { const state = { manifests: { x: { json: manifestFixtureWithAProvider } } }; - const received = getManifestProvider(state, { manifestId: 'x' }); + const received = getManifestProviderName(state, { manifestId: 'x' }); expect(received).toBe('Example Organization'); }); it('should return undefined if manifest undefined', () => { - const received = getManifestProvider({ manifests: {} }, { manifestId: 'x' }); + const received = getManifestProviderName({ manifests: {} }, { manifestId: 'x' }); expect(received).toBeUndefined(); }); }); diff --git a/__tests__/utils/test-utils.js b/__tests__/utils/test-utils.js new file mode 100644 index 0000000000000000000000000000000000000000..7104dec6b038df0a91a8744b5378e22aeab848e9 --- /dev/null +++ b/__tests__/utils/test-utils.js @@ -0,0 +1,45 @@ +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react'; +import PropTypes from 'prop-types'; +import { createStore, applyMiddleware } from 'redux'; +import thunkMiddleware from 'redux-thunk'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; +import createRootReducer from '../../src/state/reducers/rootReducer'; +import settings from '../../src/config/settings'; + +const rootReducer = createRootReducer(); +const theme = createTheme(settings.theme); + +/** + * Hook up our rendered object to redux + */ +function renderWithProviders( + ui, + { + preloadedState = {}, + // Automatically create a store instance if no store was passed in + store = createStore(rootReducer, preloadedState, applyMiddleware(thunkMiddleware)), + ...renderOptions + } = {}, +) { + /** :nodoc: */ + function Wrapper({ children }) { + return <ThemeProvider theme={theme}><Provider store={store}>{children}</Provider></ThemeProvider>; + } + + Wrapper.propTypes = { + children: PropTypes.node.isRequired, + }; + + const rendered = render(ui, { wrapper: Wrapper, ...renderOptions }); + + // Return an object with the store and all of RTL's query functions + return { + store, + ...rendered, + rerender: (newUi, options) => render(newUi, { container: rendered.container, wrapper: Wrapper, ...options }), + }; +} + +export * from '@testing-library/react'; +export { renderWithProviders as render }; diff --git a/jest-puppeteer.config.js b/jest-puppeteer.config.js index 2054bd13e9923379f82d69d26cf371698037613d..31bf265ad41b8b662bbac4fa8cb0062e7652777a 100644 --- a/jest-puppeteer.config.js +++ b/jest-puppeteer.config.js @@ -1,7 +1,6 @@ module.exports = { launch: { - args: ['--no-sandbox', '--disable-setuid-sandbox'], - headless: process.env.HEADLESS !== 'false', + headless: process.env.HEADLESS !== 'false' ? 'new' : false, }, server: [{ command: 'npm run server -- -p 4488', diff --git a/jest.json b/jest.json index fe27e7219ff26465259ead21d7ccfb9ab3695808..947d95cd44f04a7c4501be996ca01b82001e7832 100644 --- a/jest.json +++ b/jest.json @@ -6,20 +6,62 @@ "!src/containers/**/*.{js,jsx}" ], "coverageDirectory": "<rootDir>/coverage", - "coverageReporters": ["html", "lcov"], - "moduleNameMapper": { - "\\.s?css$": "<rootDir>/__mocks__/css.js", - "^uuid$": "uuid" - }, - "setupFiles": [ - "<rootDir>/setupJest.js" + "coverageReporters": [ + "html", + "lcov" ], - "setupFilesAfterEnv": [ - "<rootDir>/setup-expect-timeout.js" - ], - "testMatch": [ - "<rootDir>/**/__tests__/**/*.{js,jsx}", - "<rootDir>/src/**/?(*.)(spec|test|unit).{js,jsx}" - ], - "preset": "jest-puppeteer" + "projects": [{ + "displayName": "unit", + "moduleDirectories": [ + "node_modules", + "<rootDir>/__tests__/utils", + "<rootDir>" + ], + "moduleNameMapper": { + "\\.s?css$": "<rootDir>/__mocks__/css.js", + "^uuid$": "uuid" + }, + "setupFiles": [ + "<rootDir>/setupJest.js" + ], + "testMatch": [ + "<rootDir>/**/__tests__/**/*.{js,jsx}", + "<rootDir>/src/**/?(*.)(spec|test|unit).{js,jsx}" + ], + "testPathIgnorePatterns": [ + "<rootDir>/__tests__/integration", + "<rootDir>/__tests__/utils" + ], + "preset": "jest-puppeteer", + "setupFilesAfterEnv": ["@testing-library/jest-dom/jest-globals"], + "transformIgnorePatterns": [ + "<rootDir>/node_modules/(?!@react-dnd|react-dnd|dnd-core|react-dnd-html5-backend|dnd-multi-backend|rdndmb-html5-to-touch)" + ], + "testEnvironment": "jsdom" + }, { + "displayName": "integration", + "moduleDirectories": [ + "node_modules", + "<rootDir>/__tests__/utils", + "<rootDir>" + ], + "moduleNameMapper": { + "\\.s?css$": "<rootDir>/__mocks__/css.js", + "^uuid$": "uuid" + }, + "setupFiles": [ + "<rootDir>/setupJest.js" + ], + "testMatch": [ + "<rootDir>/**/__tests__/integration/**/*.{js,jsx}" + ], + "preset": "jest-puppeteer", + "setupFilesAfterEnv": [ + "<rootDir>/setupJestIntegration.js", + "@testing-library/jest-dom/jest-globals" + ], + "transformIgnorePatterns": [ + "<rootDir>/node_modules/(?!@react-dnd|react-dnd|dnd-core|react-dnd-html5-backend|dnd-multi-backend|rdndmb-html5-to-touch)" + ] + }] } diff --git a/package.json b/package.json index 48eb67c9300851464cb310b899099181ca1cacdf..2c4f1ec12f9e0c3b93a40e6bddb666f0ee7b0990 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "dist/cjs/src/index.js", "module": "dist/es/src/index.js", "files": [ - "dist/**" + "dist" ], "sideEffects": false, "scripts": { @@ -35,35 +35,38 @@ ], "repository": "https://github.com/ProjectMirador/mirador", "dependencies": { + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", "@hello-pangea/dnd": "^16.0.1", - "@material-ui/core": "^4.12.3", - "@material-ui/icons": "^4.9.1", - "@material-ui/lab": "^4.0.0-alpha.53", + "@mui/icons-material": "^5.11.16", + "@mui/lab": "^5.0.0-alpha.134", + "@mui/material": "^5.13.5", + "@mui/utils": "^5.13.1", + "@mui/x-tree-view": "^6.17.0", + "@react-aria/live-announcer": "^3.1.2", + "@redux-devtools/extension": "^3.3.0", "classnames": "^2.2.6", - "clsx": "^1.0.4", "deepmerge": "^4.2.2", - "dompurify": "^2.0.11", - "i18next": "^21.0.0 || ^22.0.0", - "icomcom-react": "^1.0.1", - "jss": "^10.3.0", - "jss-rtl": "^0.3.0", + "dompurify": "^3.0.0", + "i18next": "^21.0.0 || ^22.0.0 || ^23.0.0", "lodash": "^4.17.11", "manifesto.js": "^4.2.0", "normalize-url": "^4.5.0", - "openseadragon": "^2.4.2", + "openseadragon": "^2.4.2 || ^3.0.0 || ^4.0.0", "prop-types": "^15.6.2", + "rdndmb-html5-to-touch": "^8.0.0", "re-reselect": "^4.0.0", - "react-aria-live": "^2.0.5", "react-copy-to-clipboard": "^5.0.1", - "react-dnd": "^10.0.2", - "react-dnd-html5-backend": "^10.0.2", - "react-dnd-multi-backend": "^5.0.0", - "react-dnd-touch-backend": "^10.0.2", + "react-dnd": "^16.0.0", + "react-dnd-html5-backend": "^16.0.0", + "react-dnd-multi-backend": "^8.0.0", + "react-dnd-touch-backend": "^16.0.0", "react-full-screen": "^1.1.1", - "react-i18next": "^11.7.0 || ^12.0.0", + "react-i18next": "^11.7.0 || ^12.0.0 || ^13.0.0", "react-image": "^4.0.1", "react-intersection-observer": "^9.0.0", - "react-mosaic-component": "^4.0.1", + "react-mosaic-component": "^6.0.0", "react-redux": "^7.1.0 || ^8.0.0", "react-resize-observer": "^1.1.1", "react-rnd": "^10.1", @@ -75,6 +78,8 @@ "redux-saga": "^1.1.3", "redux-thunk": "^2.3.0", "reselect": "^4.0.0", + "stylis": "^4.3.0", + "stylis-plugin-rtl": "^2.1.1", "url": "^0.11.0", "uuid": "^8.1.0 || ^9.0.0" }, @@ -83,11 +88,16 @@ "@babel/core": "^7.17.7", "@babel/plugin-proposal-class-properties": "^7.16.7", "@babel/plugin-proposal-object-rest-spread": "^7.17.3", + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-transform-regenerator": "^7.16.7", "@babel/plugin-transform-runtime": "^7.17.0", "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.16.7", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", + "@testing-library/dom": "^9.2.0", + "@testing-library/jest-dom": "^6.1.5", + "@testing-library/react": "^12.1.5", + "@testing-library/user-event": "^14.4.3", "@typescript-eslint/eslint-plugin": "^5.15.0", "@typescript-eslint/parser": "^5.15.0", "babel-jest": "^29.3.1", @@ -96,30 +106,31 @@ "babel-plugin-macros": "^3.0.1", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "bundlewatch": "^0.3.3", + "canvas": "^2.11.0", "chalk": "^4.1.0", "core-js": "^3.21.1", - "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.0", "eslint": "^8.11.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-react-app": "^7.0.0", "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-import": "^2.25.4", "eslint-plugin-jest": "^27.1.5", + "eslint-plugin-jest-dom": "^5.1.0", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-react": "^7.29.4", - "eslint-plugin-react-hooks": "^4.2.0", - "expect-puppeteer": "^6.1.1", - "glob": "^8.0.3", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-testing-library": "^5.10.2", + "glob": "^10.3.0", "http-server": "^14.1.0", "jest": "^29.3.1", + "jest-environment-jsdom": "^29.4.3", "jest-fetch-mock": "^3.0.0", - "jest-junit": "^15.0.0", - "jest-puppeteer": "^6.1.0", - "jsdom": "^21.0.0", - "puppeteer": "^13.7.0", - "react": "^16.14.0", - "react-dom": "^16.14.0", + "jest-puppeteer": "^9.0.2", + "jsdom": "^23.0.0", + "puppeteer": "^21.0.0", + "react": "^17.0.0", + "react-dnd-test-backend": "^16.0.1", + "react-dom": "^17.0.0", "react-refresh": "^0.14.0", "redux-mock-store": "^1.5.1", "redux-saga-test-plan": "^4.0.0-rc.3", @@ -129,7 +140,7 @@ "webpack-dev-server": "^4.7.4" }, "peerDependencies": { - "react": "^16.14.0", - "react-dom": "^16.14.0" + "react": "^17.0.0", + "react-dom": "^17.0.0" } } diff --git a/scripts/container-lint.js b/scripts/container-lint.js index 7ef296ba57479c6c643c602e23a3042d50fce3ae..233e4588b067e099bb78912d678123f72d923106 100644 --- a/scripts/container-lint.js +++ b/scripts/container-lint.js @@ -1,5 +1,5 @@ -const glob = require('glob'); // eslint-disable-line import/no-extraneous-dependencies const fs = require('fs'); +const glob = require('glob'); // eslint-disable-line import/no-extraneous-dependencies const chalk = require('chalk'); // eslint-disable-line import/no-extraneous-dependencies const { error } = console; diff --git a/scripts/i18n-lint.js b/scripts/i18n-lint.js index 6a407f3e32706af68886d7c517821ce5963aac9a..8035c7bc1ddb0d3d19a744ceba5bfdbff689af1b 100644 --- a/scripts/i18n-lint.js +++ b/scripts/i18n-lint.js @@ -1,5 +1,5 @@ -const glob = require('glob'); // eslint-disable-line import/no-extraneous-dependencies const fs = require('fs'); +const glob = require('glob'); // eslint-disable-line import/no-extraneous-dependencies const chalk = require('chalk'); // eslint-disable-line import/no-extraneous-dependencies const { log } = console; @@ -30,10 +30,10 @@ function unsortedKeys(arr) { * (values will be sorted and downcased for comparison) */ function missingKeys(arr1, arr2) { - const sortedDonwcasedLeftHandArray = lowerCaseSortedArray(arr1); - const sortedDonwcasedRightHandArray = lowerCaseSortedArray(arr2); + const sortedDowncasedLeftHandArray = lowerCaseSortedArray(arr1); + const sortedDowncasedRightHandArray = lowerCaseSortedArray(arr2); - return sortedDonwcasedLeftHandArray.filter((v, i) => v !== sortedDonwcasedRightHandArray[i]); + return sortedDowncasedLeftHandArray.filter((v, i) => !sortedDowncasedRightHandArray.includes(v)); } files.forEach((fileName) => { diff --git a/setupJest.js b/setupJest.js index 28711b408f94cac3ec49a50181026c221ae71018..d5a066407502df21961cb96ffc7ce0c43dedd4dd 100644 --- a/setupJest.js +++ b/setupJest.js @@ -1,62 +1,44 @@ -// Setup Jest to mock fetch - -import { JSDOM } from 'jsdom'; // eslint-disable-line import/no-extraneous-dependencies -import fetchMock from 'jest-fetch-mock'; // eslint-disable-line import/no-extraneous-dependencies -import Enzyme from 'enzyme'; // eslint-disable-line import/no-extraneous-dependencies -import Adapter from 'enzyme-adapter-react-16'; // eslint-disable-line import/no-extraneous-dependencies - -const jsdom = new JSDOM('<!doctype html><html><body><div id="main"></div></body></html>', { url: 'https://localhost' }); -const { window } = jsdom; +/* eslint-disable import/no-extraneous-dependencies */ +import fetchMock from 'jest-fetch-mock'; +import sizeMe from 'react-sizeme'; +import i18next from 'i18next'; +import { setupIntersectionMocking } from 'react-intersection-observer/test-utils'; +import en from './src/locales/en/translation.json'; jest.setTimeout(10000); -window.HTMLCanvasElement.prototype.getContext = () => {}; -fetchMock.enableMocks(); +sizeMe.noPlaceholders = true; -global.window = window; -global.navigator = { - userAgent: 'node.js', -}; +const { TextEncoder } = require('util'); -/* eslint-disable require-jsdoc, class-methods-use-this */ -class IntersectionObserverPolyfill { - observe() { - } +global.TextEncoder = TextEncoder; - disconnect() { - } -} -/* eslint-enable require-jsdoc, class-methods-use-this */ +// Setup Jest to mock fetch +fetchMock.enableMocks(); + +if (typeof Element !== 'undefined') Element.prototype.scrollTo = () => {}; -global.IntersectionObserver = IntersectionObserverPolyfill; +setupIntersectionMocking(jest.fn); /** */ function Path2D() { } global.Path2D = Path2D; -/** - * copy object property descriptors from `src` to `target` - * @param {*} src - * @param {*} target - */ -const copyProps = (src, target) => { - Object.defineProperties(target, { - ...Object.getOwnPropertyDescriptors(src), - ...Object.getOwnPropertyDescriptors(target), - }); -}; - -/* - avoid 'ReferenceError: HTMLElement is not defined' - see https://github.com/airbnb/enzyme/blob/master/docs/guides/jsdom.md - for further information -*/ -copyProps(window, global); -Enzyme.configure({ adapter: new Adapter() }); +i18next.init({ + lng: 'en', + resources: { + en, + }, +}); jest.mock('react-i18next', () => ({ + I18nextProvider: ({ children }) => children, + initReactI18next: { + init: jest.fn(), + type: '3rdParty', + }, // this mock makes sure any components using the translate HoC receive the t function as a prop withTranslation: () => (Component) => { Component.defaultProps = { // eslint-disable-line no-param-reassign diff --git a/setupJestIntegration.js b/setupJestIntegration.js new file mode 100644 index 0000000000000000000000000000000000000000..0936be7363020a4392a9d4e7d560f311969b577f --- /dev/null +++ b/setupJestIntegration.js @@ -0,0 +1,3 @@ +import { setDefaultOptions } from 'expect-puppeteer'; // eslint-disable-line import/no-extraneous-dependencies + +setDefaultOptions({ timeout: 10000 }); diff --git a/src/components/AccessTokenSender.js b/src/components/AccessTokenSender.js index 99a396fea584073d57602418a2bb21de529ef759..a17368488b18d02d1866ab825cc7de72f92715d1 100644 --- a/src/components/AccessTokenSender.js +++ b/src/components/AccessTokenSender.js @@ -1,6 +1,6 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import IComCom from 'icomcom-react'; +import { IIIFIFrameCommunication } from './IIIFIFrameCommunication'; /** * Opens a new window for click @@ -29,14 +29,9 @@ export class AccessTokenSender extends Component { external, no-op */ return ( - <IComCom - attributes={{ - 'aria-hidden': true, - height: 1, - src: `${url}?origin=${window.origin}&messageId=${url}`, - style: { visibility: 'hidden' }, - width: 1, - }} + <IIIFIFrameCommunication + src={`${url}?origin=${window.origin}&messageId=${url}`} + title="AccessTokenSender" handleReceiveMessage={this.onReceiveAccessTokenMessage} /> ); diff --git a/src/components/AnnotationSettings.js b/src/components/AnnotationSettings.js index 75ed01d5fb51bda5b0620bfa19adf3204461b0c1..435edc35436bf4cdde0a2466e1d887883499f18c 100644 --- a/src/components/AnnotationSettings.js +++ b/src/components/AnnotationSettings.js @@ -1,9 +1,9 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import SyncIcon from '@material-ui/icons/Sync'; -import SyncDisabledIcon from '@material-ui/icons/SyncDisabled'; -import VisibilityIcon from '@material-ui/icons/VisibilitySharp'; -import VisibilityOffIcon from '@material-ui/icons/VisibilityOffSharp'; +import VisibilityIcon from '@mui/icons-material/VisibilitySharp'; +import VisibilityOffIcon from '@mui/icons-material/VisibilityOffSharp'; +import SyncIcon from '@mui/icons-material/Sync'; +import SyncIcon from '@mui/icons-material/SyncDisabled'; import MiradorMenuButton from '../containers/MiradorMenuButton'; import { VideosReferences } from '../plugins/VideosReferences'; diff --git a/src/components/AnnotationsOverlay.js b/src/components/AnnotationsOverlay.js index f68ced24754f36ead80cec7b5dca64c272f63eb8..a4a44c66e958ac08135c62c404723691002aa9ac 100644 --- a/src/components/AnnotationsOverlay.js +++ b/src/components/AnnotationsOverlay.js @@ -126,6 +126,7 @@ export class AnnotationsOverlay extends Component { componentWillUnmount() { const { viewer } = this.props; + this.onCanvasMouseMove.cancel(); viewer.removeHandler('canvas-click', this.onCanvasClick); viewer.removeHandler('canvas-exit', this.onCanvasExit); viewer.removeHandler('update-viewport', this.onUpdateViewport); diff --git a/src/components/App.js b/src/components/App.js index 9c12117fc85c5cb09864af8470894d2b7db60bd4..0b57527c1ff0797840976c28667ef89f0924baf8 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,7 +1,10 @@ -import { Component, lazy, Suspense } from 'react'; +import { + createRef, Component, lazy, Suspense, +} from 'react'; import PropTypes from 'prop-types'; import PluginProvider from '../extend/PluginProvider'; import AppProviders from '../containers/AppProviders'; +import WorkspaceContext from '../contexts/WorkspaceContext'; const WorkspaceArea = lazy(() => import('../containers/WorkspaceArea')); @@ -10,6 +13,13 @@ const WorkspaceArea = lazy(() => import('../containers/WorkspaceArea')); * @prop {Object} manifests */ export class App extends Component { + /** */ + constructor(props) { + super(props); + + this.areaRef = createRef(); + } + /** * render * @return {String} - HTML markup for the component @@ -20,11 +30,13 @@ export class App extends Component { return ( <PluginProvider plugins={plugins}> <AppProviders dndManager={dndManager}> - <Suspense - fallback={<div />} - > - <WorkspaceArea /> - </Suspense> + <WorkspaceContext.Provider value={this.areaRef}> + <Suspense + fallback={<div />} + > + <WorkspaceArea areaRef={this.areaRef} /> + </Suspense> + </WorkspaceContext.Provider> </AppProviders> </PluginProvider> ); diff --git a/src/components/AppProviders.js b/src/components/AppProviders.js index aab7a26ed7b183e3360551a29680b10a33df9d68..f39aae60984e174a60d8a569b591bc78b6275753 100644 --- a/src/components/AppProviders.js +++ b/src/components/AppProviders.js @@ -2,15 +2,16 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; import { FullScreen, useFullScreenHandle } from 'react-full-screen'; import { I18nextProvider } from 'react-i18next'; -import { LiveAnnouncer } from 'react-aria-live'; import { - ThemeProvider, StylesProvider, createTheme, jssPreset, createGenerateClassName, -} from '@material-ui/core/styles'; + ThemeProvider, StyledEngineProvider, createTheme, +} from '@mui/material/styles'; import { DndContext, DndProvider } from 'react-dnd'; -import MultiBackend from 'react-dnd-multi-backend'; -import HTML5toTouch from 'react-dnd-multi-backend/dist/cjs/HTML5toTouch'; -import { create } from 'jss'; -import rtl from 'jss-rtl'; +import { MultiBackend } from 'react-dnd-multi-backend'; +import { HTML5toTouch } from 'rdndmb-html5-to-touch'; +import rtlPlugin from 'stylis-plugin-rtl'; +import { prefixer } from 'stylis'; +import { CacheProvider } from '@emotion/react'; +import createCache from '@emotion/cache'; import createI18nInstance from '../i18n'; import FullScreenContext from '../contexts/FullScreenContext'; @@ -76,15 +77,8 @@ export class AppProviders extends Component { super(props); this.i18n = createI18nInstance(); - } - - /** - * Set i18n language on component mount - */ - componentDidMount() { - const { language } = this.props; - - this.i18n.changeLanguage(language); + // Set i18n language + this.i18n.changeLanguage(props.language); } /** @@ -92,7 +86,6 @@ export class AppProviders extends Component { */ componentDidUpdate(prevProps) { const { language } = this.props; - if (prevProps.language !== language) { this.i18n.changeLanguage(language); } @@ -101,12 +94,25 @@ export class AppProviders extends Component { /** */ render() { const { - children, createGenerateClassNameOptions, + children, theme, translations, dndManager, } = this.props; - const generateClassName = createGenerateClassName(createGenerateClassNameOptions); + /** + * Create rtl emotion cache + */ + const cacheRtl = createCache({ + key: 'muirtl', + stylisPlugins: [prefixer, rtlPlugin], + }); + + /** + * Create default emotion cache + */ + const cacheDefault = createCache({ + key: 'mui', + }); Object.keys(translations).forEach((lng) => { this.i18n.addResourceBundle(lng, 'translation', translations[lng], true, true); @@ -115,20 +121,15 @@ export class AppProviders extends Component { return ( <FullScreenShim> <I18nextProvider i18n={this.i18n}> - <LiveAnnouncer> - <ThemeProvider - theme={createTheme(theme)} - > - <StylesProvider - jss={create({ plugins: [...jssPreset().plugins, rtl()] })} - generateClassName={generateClassName} - > + <StyledEngineProvider injectFirst> + <CacheProvider value={theme.direction === 'rtl' ? cacheRtl : cacheDefault}> + <ThemeProvider theme={createTheme((theme))}> <MaybeDndProvider dndManager={dndManager}> {children} </MaybeDndProvider> - </StylesProvider> - </ThemeProvider> - </LiveAnnouncer> + </ThemeProvider> + </CacheProvider> + </StyledEngineProvider> </I18nextProvider> </FullScreenShim> ); @@ -137,7 +138,6 @@ export class AppProviders extends Component { AppProviders.propTypes = { children: PropTypes.node, - createGenerateClassNameOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types dndManager: PropTypes.object, // eslint-disable-line react/forbid-prop-types language: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types @@ -146,6 +146,5 @@ AppProviders.propTypes = { AppProviders.defaultProps = { children: null, - createGenerateClassNameOptions: {}, dndManager: undefined, }; diff --git a/src/components/AttributionPanel.js b/src/components/AttributionPanel.js index c2d8cc6979e1332c0b9ef46a4c0369209d645342..b1d9c6ebec7acf6af795a26c83c8e205d0df8e1d 100644 --- a/src/components/AttributionPanel.js +++ b/src/components/AttributionPanel.js @@ -1,14 +1,24 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Typography from '@material-ui/core/Typography'; -import Link from '@material-ui/core/Link'; -import Skeleton from '@material-ui/lab/Skeleton'; +import { styled } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; +import Link from '@mui/material/Link'; +import Skeleton from '@mui/material/Skeleton'; import { Img } from 'react-image'; import CompanionWindow from '../containers/CompanionWindow'; +import { CompanionWindowSection } from './CompanionWindowSection'; import { LabelValueMetadata } from './LabelValueMetadata'; import ns from '../config/css-ns'; import { PluginHook } from './PluginHook'; +const StyledLogo = styled(Img)(() => ({ + maxWidth: '100%', +})); + +const StyledPlaceholder = styled(Skeleton)(({ theme }) => ({ + backgroundColor: theme.palette.grey[300], +})); + /** * WindowSideBarInfoPanel */ @@ -24,7 +34,6 @@ export class AttributionPanel extends Component { rights, windowId, id, - classes, t, } = this.props; @@ -35,7 +44,7 @@ export class AttributionPanel extends Component { windowId={windowId} id={id} > - <div className={classes.section}> + <CompanionWindowSection> { requiredStatement && ( <LabelValueMetadata labelValuePairs={requiredStatement} defaultLabel={t('attribution')} /> )} @@ -53,20 +62,19 @@ export class AttributionPanel extends Component { </dl> ) } - </div> + </CompanionWindowSection> { manifestLogo && ( - <div className={classes.section}> - <Img + <CompanionWindowSection> + <StyledLogo src={[manifestLogo]} alt="" role="presentation" - className={classes.logo} unloader={ - <Skeleton className={classes.placeholder} variant="rect" height={60} width={60} /> + <StyledPlaceholder variant="rectangular" height={60} width={60} /> } /> - </div> + </CompanionWindowSection> )} <PluginHook {...this.props} /> @@ -76,7 +84,6 @@ export class AttributionPanel extends Component { } AttributionPanel.propTypes = { - classes: PropTypes.objectOf(PropTypes.string), id: PropTypes.string.isRequired, manifestLogo: PropTypes.string, requiredStatement: PropTypes.arrayOf(PropTypes.shape({ @@ -89,7 +96,6 @@ AttributionPanel.propTypes = { }; AttributionPanel.defaultProps = { - classes: {}, manifestLogo: null, requiredStatement: null, rights: null, diff --git a/src/components/AudioViewer.js b/src/components/AudioViewer.js index ab7b9d6883b7186b6b990744707d5be5923eab32..9ceb07d78826542af7e9133df4c66f4ea42232a4 100644 --- a/src/components/AudioViewer.js +++ b/src/components/AudioViewer.js @@ -1,40 +1,48 @@ -import { Component, Fragment } from 'react'; +import { Fragment } from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; + +const StyledContainer = styled('div')({ + alignItems: 'center', + display: 'flex', + width: '100%', +}); + +const StyledAudio = styled('audio')({ + width: '100%', +}); /** */ -export class AudioViewer extends Component { +export function AudioViewer(props) { /* eslint-disable jsx-a11y/media-has-caption */ /** */ - render() { - const { - captions, classes, audioOptions, audioResources, - } = this.props; + const { + captions, audioOptions, audioResources, + } = props; - return ( - <div className={classes.container}> - <audio className={classes.audio} {...audioOptions}> - {audioResources.map(audio => ( - <Fragment key={audio.id}> - <source src={audio.id} type={audio.getFormat()} /> - </Fragment> - ))} - {captions.map(caption => ( - <Fragment key={caption.id}> - <track src={caption.id} label={caption.getDefaultLabel()} srcLang={caption.getProperty('language')} /> - </Fragment> - ))} - </audio> - </div> - ); - } - /* eslint-enable jsx-a11y/media-has-caption */ + return ( + <StyledContainer> + <StyledAudio {...audioOptions}> + {audioResources.map((audio) => ( + <Fragment key={audio.id}> + <source src={audio.id} type={audio.getFormat()} /> + </Fragment> + ))} + {captions.map((caption) => ( + <Fragment key={caption.id}> + <track src={caption.id} label={caption.getDefaultLabel()} srcLang={caption.getProperty('language')} /> + </Fragment> + ))} + </StyledAudio> + </StyledContainer> + ); } +/* eslint-enable jsx-a11y/media-has-caption */ AudioViewer.propTypes = { audioOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types audioResources: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types captions: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types - classes: PropTypes.objectOf(PropTypes.string).isRequired, }; AudioViewer.defaultProps = { diff --git a/src/components/Branding.js b/src/components/Branding.js index d2f4d4f9e1a42cd605136baa9da81cba0526586e..9b3af649ccbb0520a5842e95778b45091a97205b 100644 --- a/src/components/Branding.js +++ b/src/components/Branding.js @@ -1,7 +1,8 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import IconButton from '@material-ui/core/IconButton'; -import Typography from '@material-ui/core/Typography'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; import MiradorIcon from './icons/MiradorIcon'; /** @@ -13,7 +14,7 @@ export class Branding extends Component { const { t, variant, ...ContainerProps } = this.props; return ( - <div {...ContainerProps}> + <Stack alignItems="center" {...ContainerProps}> { variant === 'wide' && ( <div> <Typography align="center" component="p" variant="h3">{t('mirador')}</Typography> @@ -25,11 +26,12 @@ export class Branding extends Component { href="https://projectmirador.org" target="_blank" rel="noopener" + size="large" > <MiradorIcon aria-label={t('aboutMirador')} titleAccess={t('aboutMirador')} fontSize="large" /> </IconButton> </Typography> - </div> + </Stack> ); } } diff --git a/src/components/CanvasAnnotations.js b/src/components/CanvasAnnotations.js index 89ee726e6471faaf212c41ede26b04117b7c8524..12320729071386d2e25b6ef7e2a1b0f42259cadf 100644 --- a/src/components/CanvasAnnotations.js +++ b/src/components/CanvasAnnotations.js @@ -1,12 +1,12 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import Chip from '@material-ui/core/Chip'; -import MenuList from '@material-ui/core/MenuList'; -import MenuItem from '@material-ui/core/MenuItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import Typography from '@material-ui/core/Typography'; -import SearchIcon from '@material-ui/icons/SearchSharp'; +import Chip from '@mui/material/Chip'; +import MenuList from '@mui/material/MenuList'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemText from '@mui/material/ListItemText'; +import Typography from '@mui/material/Typography'; +import SearchIcon from '@mui/icons-material/Search'; import TextField from '@material-ui/core/TextField'; import SanitizedHtml from '../containers/SanitizedHtml'; import { ScrollTo } from './ScrollTo'; @@ -71,13 +71,10 @@ export class CanvasAnnotations extends Component { */ render() { const { - autoScroll, classes, index, label, selectedAnnotationId, t, totalSize, + autoscroll, annotations, classes, index, label, selectedAnnotationId, t, totalSize, listContainerComponent, htmlSanitizationRuleSet, hoveredAnnotationIds, containerRef, } = this.props; - - let { annotations } = this.props; - if (annotations.length === 0) return null; const { inputSearch } = this.state; @@ -107,23 +104,25 @@ export class CanvasAnnotations extends Component { /> </div> <MenuList autoFocusItem variant="selectedMenu"> - { - annotations.map(annotation => ( + {annotations.map((annotation) => ( <ScrollTo containerRef={containerRef} key={`${annotation.id}-scroll`} offsetTop={96} // offset for the height of the form above - scrollTo={autoScroll ? (selectedAnnotationId === annotation.id) : false} + scrollTo={autoScroll ? (selectedAnnotationId === annotation.id)} + selected={selectedAnnotationId === annotation.id} > <MenuItem button component={listContainerComponent} - className={clsx( - classes.annotationListItem, - { - [classes.hovered]: hoveredAnnotationIds.includes(annotation.id), + variant="multiline" + divider + sx={{ + '&:hover,&:focus': { + backgroundColor: 'action.hover', }, - )} + backgroundColor: hoveredAnnotationIds.includes(annotation.id) ? 'action.hover' : '', + }} key={annotation.id} annotationid={annotation.id} selected={selectedAnnotationId === annotation.id} @@ -133,35 +132,25 @@ export class CanvasAnnotations extends Component { onMouseEnter={() => this.handleAnnotationHover(annotation)} onMouseLeave={this.handleAnnotationBlur} > - <ListItemText primaryTypographyProps={{ variant: 'body2' }}> - <SanitizedHtml - ruleSet={htmlSanitizationRuleSet} - htmlString={annotation.content} + <ListItemText + primaryTypographyProps={{ variant: 'body2' }} + primary={ + <SanitizedHtml ruleSet={htmlSanitizationRuleSet} htmlString={annotation.content} /> + } + secondary={ + annotation.tags.map((tag) => ( + <Chip component="span" size="small" variant="outlined" label={tag} id={tag} key={tag.toString()} /> + )) + } + <AnnotationManifestsAccordion + annotation={annotation} + t={t} /> - <div> - { - annotation.tags.map(tag => ( - <Chip size="small" variant="outlined" label={tag} id={tag} className={classes.chip} key={tag.toString()} /> - )) - } - <AnnotationManifestsAccordion - annotation={annotation} - t={t} - /> - </div> </ListItemText> </MenuItem> </ScrollTo> )) } - {annotations.length == 0 - && ( - <MenuItem> - <Typography> - {t('noAnnotationFound')} - </Typography> - </MenuItem> - )} </MenuList> <div className={classes.footerAnnotationPanel}> <Typography component="p" variant="subtitle2">{t('showingNumAnnotations', { count: annotationCount, number: annotationCount })}</Typography> diff --git a/src/components/CanvasInfo.js b/src/components/CanvasInfo.js index 93f4b31243d6b9e5b33632085d6b53c4b3e3207c..39ec1a03fd29aeb92d23f60e2be2d672427bac5b 100644 --- a/src/components/CanvasInfo.js +++ b/src/components/CanvasInfo.js @@ -1,6 +1,6 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Typography from '@material-ui/core/Typography'; +import Typography from '@mui/material/Typography'; import CollapsibleSection from '../containers/CollapsibleSection'; import SanitizedHtml from '../containers/SanitizedHtml'; import { LabelValueMetadata } from './LabelValueMetadata'; diff --git a/src/components/CanvasLayers.js b/src/components/CanvasLayers.js index 99e400ca2573b8237ef3137fa27712764c9b6d81..87f78490043e3700c25a0572f61f996372b18e7e 100644 --- a/src/components/CanvasLayers.js +++ b/src/components/CanvasLayers.js @@ -1,23 +1,36 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import clsx from 'clsx'; +import { styled } from '@mui/material/styles'; import { v4 as uuid } from 'uuid'; -import Input from '@material-ui/core/Input'; -import InputAdornment from '@material-ui/core/InputAdornment'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import Slider from '@material-ui/core/Slider'; -import Tooltip from '@material-ui/core/Tooltip'; -import DragHandleIcon from '@material-ui/icons/DragHandleSharp'; -import MoveToTopIcon from '@material-ui/icons/VerticalAlignTopSharp'; -import VisibilityIcon from '@material-ui/icons/VisibilitySharp'; -import VisibilityOffIcon from '@material-ui/icons/VisibilityOffSharp'; -import OpacityIcon from '@material-ui/icons/OpacitySharp'; -import Typography from '@material-ui/core/Typography'; +import Input from '@mui/material/Input'; +import InputAdornment from '@mui/material/InputAdornment'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import Slider from '@mui/material/Slider'; +import Tooltip from '@mui/material/Tooltip'; +import DragHandleIcon from '@mui/icons-material/DragHandleSharp'; +import MoveToTopIcon from '@mui/icons-material/VerticalAlignTopSharp'; +import VisibilityIcon from '@mui/icons-material/VisibilitySharp'; +import VisibilityOffIcon from '@mui/icons-material/VisibilityOffSharp'; +import OpacityIcon from '@mui/icons-material/OpacitySharp'; +import Typography from '@mui/material/Typography'; import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; import MiradorMenuButton from '../containers/MiradorMenuButton'; import IIIFThumbnail from '../containers/IIIFThumbnail'; +const StyledDragHandle = styled('div')(({ theme }) => ({ + alignItems: 'center', + borderRight: `0.5px solid ${theme.palette.divider}`, + display: 'flex', + flex: 1, + flexDirection: 'row', + marginBottom: theme.spacing(-2), + marginRight: theme.spacing(1), + marginTop: theme.spacing(-2), + maxWidth: theme.spacing(3), + width: theme.spacing(3), +})); + /** */ const reorder = (list, startIndex, endIndex) => { const result = Array.from(list); @@ -116,7 +129,6 @@ export class CanvasLayers extends Component { /** @private */ renderLayer(resource, index) { const { - classes, layerMetadata, t, } = this.props; @@ -136,10 +148,12 @@ export class CanvasLayers extends Component { maxHeight={height} maxWidth={width} resource={resource} - classes={{ image: classes.image, root: classes.thumbnail }} + border /> <Typography - className={classes.label} + sx={{ + paddingLeft: 1, + }} component="div" variant="body1" > @@ -158,10 +172,21 @@ export class CanvasLayers extends Component { </div> <div style={{ alignItems: 'center', display: 'flex' }}> <Tooltip title={t('layer_opacity')}> - <OpacityIcon className={classes.opacityIcon} color={layer.visibility ? 'inherit' : 'disabled'} fontSize="small" /> + <OpacityIcon sx={{ marginRight: 0.5 }} color={layer.visibility ? 'inherit' : 'disabled'} fontSize="small" /> </Tooltip> <Input - classes={{ input: classes.opacityInput }} + sx={{ + 'MuiInput-input': { + '&::-webkit-outer-spin-button,&::-webkit-inner-spin-button': { + margin: 0, + WebkitAppearance: 'none', + }, + MozAppearance: 'textfield', + textAlign: 'right', + typography: 'caption', + width: '3ch', + }, + }} disabled={!layer.visibility} value={Math.round(layer.opacity * 100)} type="number" @@ -174,7 +199,11 @@ export class CanvasLayers extends Component { }} /> <Slider - className={classes.slider} + sx={{ + marginLeft: 2, + marginRight: 2, + maxWidth: 150, + }} disabled={!layer.visibility} value={layer.opacity * 100} onChange={(e, value) => this.handleOpacityChange(resource.id, value)} @@ -187,7 +216,6 @@ export class CanvasLayers extends Component { /** @private */ renderDraggableLayer(resource, index) { const { - classes, t, } = this.props; @@ -198,20 +226,33 @@ export class CanvasLayers extends Component { ref={provided.innerRef} {...provided.draggableProps} component="li" - className={clsx( - classes.listItem, - { - [classes.dragging]: snapshot.isDragging, - }, - )} + divider + sx={{ + alignItems: 'stretch', + cursor: 'pointer', + paddingBottom: 2, + paddingRight: 2, + paddingTop: 2, + ...(snapshot.isDragging && { + backgroundColor: 'action.hover', + }), + }} disableGutters key={resource.id} > - <div {...provided.dragHandleProps} className={classes.dragHandle}> + <StyledDragHandle + {...provided.dragHandleProps} + sx={{ + '&:hover': { + backgroundColor: snapshot.isDragging ? 'action.selected' : 'action.hover', + }, + backgroundColor: snapshot.isDragging ? 'action.selected' : 'shades.light', + }} + > <Tooltip title={t('layer_move')}> <DragHandleIcon /> </Tooltip> - </div> + </StyledDragHandle> {this.renderLayer(resource, index)} </ListItem> )} @@ -222,7 +263,6 @@ export class CanvasLayers extends Component { /** */ render() { const { - classes, index, label, layers, @@ -233,7 +273,14 @@ export class CanvasLayers extends Component { return ( <> { totalSize > 1 && ( - <Typography className={classes.sectionHeading} variant="overline"> + <Typography + sx={{ + paddingLeft: 1, + paddingRight: 1, + paddingTop: 2, + }} + variant="overline" + > {t('annotationCanvasLabel', { context: `${index + 1}/${totalSize}`, label })} </Typography> )} @@ -241,7 +288,9 @@ export class CanvasLayers extends Component { <Droppable droppableId={this.droppableId}> {(provided, snapshot) => ( <List - className={classes.list} + sx={{ + paddingTop: 0, + }} {...provided.droppableProps} ref={provided.innerRef} > @@ -262,7 +311,6 @@ export class CanvasLayers extends Component { CanvasLayers.propTypes = { canvasId: PropTypes.string.isRequired, - classes: PropTypes.objectOf(PropTypes.string), index: PropTypes.number.isRequired, label: PropTypes.string.isRequired, layerMetadata: PropTypes.objectOf(PropTypes.shape({ @@ -277,6 +325,5 @@ CanvasLayers.propTypes = { }; CanvasLayers.defaultProps = { - classes: {}, layerMetadata: undefined, }; diff --git a/src/components/ChangeThemeDialog.js b/src/components/ChangeThemeDialog.js index c01da4ce861c550c06d1947a78758146b824c9aa..17bd83171aab087aa3eafd88173a167674f1a3fa 100644 --- a/src/components/ChangeThemeDialog.js +++ b/src/components/ChangeThemeDialog.js @@ -1,33 +1,25 @@ import { Component } from 'react'; import { - Dialog, DialogTitle, ListItemIcon, ListItemText, MenuList, MenuItem, - Typography, DialogContent, -} from '@material-ui/core'; -import PaletteIcon from '@material-ui/icons/PaletteSharp'; +} from '@mui/material'; +import PaletteIcon from '@mui/icons-material/PaletteSharp'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; +import { WorkspaceDialog } from './WorkspaceDialog'; + +const ThemeIcon = styled(PaletteIcon, { name: 'ThemeIcon', slot: 'icon' })(({ theme }) => ({ + color: '#BDBDBD', +})); /** * a simple dialog providing the possibility to switch the theme */ export class ChangeThemeDialog extends Component { - /** - * Set the initial focus when the dialog enters - * Find the selected item by using the current theme - * in a selector on the value attribute (which we need to set) - */ - static setInitialFocus(dialogElement, selectedTheme) { - const selectedListItem = dialogElement.querySelectorAll(`li[value="${selectedTheme}"]`); - if (!selectedListItem || selectedListItem.length === 0) return; - - selectedListItem[0].focus(); - } - /** */ constructor(props) { @@ -48,7 +40,6 @@ export class ChangeThemeDialog extends Component { /** */ render() { const { - classes, handleClose, open, selectedTheme, @@ -56,43 +47,34 @@ export class ChangeThemeDialog extends Component { themeIds, } = this.props; return ( - <Dialog - onClose={handleClose} - onEntered={dialog => ChangeThemeDialog.setInitialFocus(dialog, selectedTheme)} - open={open} - > - <DialogTitle id="change-the-dialog-title" disableTypography> - <Typography variant="h2"> - {t('changeTheme')} - </Typography> + <WorkspaceDialog onClose={handleClose} open={open} variant="menu"> + <DialogTitle> + {t('changeTheme')} </DialogTitle> - <DialogContent className={classes.dialogContent}> - <MenuList> - { - themeIds.map(value => ( - <MenuItem - key={value} - className={classes.listitem} - onClick={() => { this.handleThemeChange(value); }} - selected={value === selectedTheme} - value={value} - > - <ListItemIcon> - <PaletteIcon className={classes[value]} /> - </ListItemIcon> - <ListItemText>{t(value)}</ListItemText> - </MenuItem> - )) - } + <DialogContent> + <MenuList autoFocusItem> + {themeIds.map((value) => ( + <MenuItem + key={value} + className="listitem" + onClick={() => this.handleThemeChange(value)} + selected={value === selectedTheme} + value={value} + > + <ListItemIcon> + <ThemeIcon ownerState={{ value }} /> + </ListItemIcon> + <ListItemText>{t(value)}</ListItemText> + </MenuItem> + ))} </MenuList> </DialogContent> - </Dialog> + </WorkspaceDialog> ); } } ChangeThemeDialog.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, handleClose: PropTypes.func.isRequired, open: PropTypes.bool, selectedTheme: PropTypes.string.isRequired, diff --git a/src/components/CollapsibleSection.js b/src/components/CollapsibleSection.js index 8773c9b6d951d9c14ccbb6ec3000db74bd032811..04c5f7d5137bd28367c52808d6663c1f98b60d05 100644 --- a/src/components/CollapsibleSection.js +++ b/src/components/CollapsibleSection.js @@ -1,9 +1,10 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Typography from '@material-ui/core/Typography'; -import KeyboardArrowDown from '@material-ui/icons/KeyboardArrowDownSharp'; -import KeyboardArrowUp from '@material-ui/icons/KeyboardArrowUpSharp'; -import MiradorMenuButton from '../containers/MiradorMenuButton'; +import Typography from '@mui/material/Typography'; +import Accordion from '@mui/material/Accordion'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; /** * CollapsableSection ~ @@ -14,14 +15,12 @@ export class CollapsibleSection extends Component { super(props); this.state = { open: true }; - this.toggleSection = this.toggleSection.bind(this); + this.handleChange = this.handleChange.bind(this); } - /** */ - toggleSection() { - const { open } = this.state; - - this.setState({ open: !open }); + /** Control the accordion state so we can provide aria labeling */ + handleChange(event, isExpanded) { + this.setState({ open: isExpanded }); } /** @@ -29,45 +28,28 @@ export class CollapsibleSection extends Component { */ render() { const { - children, classes, id, label, t, + children, id, label, t, } = this.props; const { open } = this.state; return ( - <> - <div className={classes.container}> - <Typography - className={classes.heading} - id={id} - onClick={this.toggleSection} - variant="overline" - component="h4" - > + <Accordion id={id} elevation={0} expanded={open} onChange={this.handleChange} disableGutters square variant="compact"> + <AccordionSummary id={`${id}-header`} aria-controls={`${id}-content`} aria-label={t(open ? 'collapseSection' : 'expandSection', { section: label })} expandIcon={<ExpandMoreIcon />}> + <Typography variant="overline" component="h4"> {label} </Typography> - <MiradorMenuButton - aria-label={ - t( - open ? 'collapseSection' : 'expandSection', - { section: label }, - ) - } - aria-expanded={open} - className={classes.button} - onClick={this.toggleSection} - > - {open ? <KeyboardArrowUp /> : <KeyboardArrowDown />} - </MiradorMenuButton> - </div> - {open && children} - </> + </AccordionSummary> + <AccordionDetails> + {children} + </AccordionDetails> + </Accordion> ); } } CollapsibleSection.propTypes = { children: PropTypes.node.isRequired, - classes: PropTypes.objectOf(PropTypes.string).isRequired, + id: PropTypes.string.isRequired, label: PropTypes.string.isRequired, t: PropTypes.func.isRequired, diff --git a/src/components/CollectionDialog.js b/src/components/CollectionDialog.js index 71ecb472ee6184448832f67dd0711f3666ae6fc5..75fa641f14cc78bced024ae8cb14736effba434c 100644 --- a/src/components/CollectionDialog.js +++ b/src/components/CollectionDialog.js @@ -10,15 +10,32 @@ import { MenuList, MenuItem, Typography, -} from '@material-ui/core'; -import ArrowBackIcon from '@material-ui/icons/ArrowBackSharp'; -import Skeleton from '@material-ui/lab/Skeleton'; +} from '@mui/material'; +import ArrowBackIcon from '@mui/icons-material/ArrowBackSharp'; +import Skeleton from '@mui/material/Skeleton'; +import { styled } from '@mui/material/styles'; import asArray from '../lib/asArray'; import { LabelValueMetadata } from './LabelValueMetadata'; import CollapsibleSection from '../containers/CollapsibleSection'; import ScrollIndicatedDialogContent from '../containers/ScrollIndicatedDialogContent'; import ManifestInfo from '../containers/ManifestInfo'; +const StyledScrollIndicatedDialogContent = styled(ScrollIndicatedDialogContent)(() => ({ + padding: (theme) => theme.spacing(1), +})); + +const StyledCollectionMetadata = styled('div')(() => ({ + '& .MuiPaper-root': { + background: 'transparent', + }, + padding: (theme) => theme.spacing(2), +})); + +const StyledCollectionFilter = styled('div')(() => ({ + padding: (theme) => theme.spacing(2), + paddingTop: 0, +})); + /** * a dialog providing the possibility to select the collection */ @@ -102,43 +119,33 @@ export class CollectionDialog extends Component { /** */ dialogContainer() { - const { containerId, windowId } = this.props; - return document.querySelector(`#${containerId} #${windowId}`); + const { container, windowId } = this.props; + return (container?.current || document.body).querySelector(`#${windowId}`); } /** */ placeholder() { - const { classes } = this.props; - return ( <Dialog - className={classes.dialog} + variant="contained" onClose={this.hideDialog} open container={this.dialogContainer()} - BackdropProps={this.backdropProps()} > - <DialogTitle id="select-collection" disableTypography> - <Skeleton className={classes.placeholder} variant="text" /> + <DialogTitle id="select-collection"> + <Skeleton variant="text" /> </DialogTitle> <ScrollIndicatedDialogContent> - <Skeleton className={classes.placeholder} variant="text" /> - <Skeleton className={classes.placeholder} variant="text" /> + <Skeleton variant="text" /> + <Skeleton variant="text" /> </ScrollIndicatedDialogContent> </Dialog> ); } - /** */ - backdropProps() { - const { classes } = this.props; - return { classes: { root: classes.dialog } }; - } - /** */ render() { const { - classes, collection, error, isMultipart, @@ -173,21 +180,20 @@ export class CollectionDialog extends Component { return ( <Dialog - className={classes.dialog} + variant="contained" onClose={this.hideDialog} container={this.dialogContainer()} - BackdropProps={this.backdropProps()} open > - <DialogTitle id="select-collection" disableTypography> + <DialogTitle id="select-collection"> <Typography component="div" variant="overline"> { t(isMultipart ? 'multipartCollection' : 'collection') } </Typography> - <Typography variant="h3"> + <Typography component="div" variant="h3"> {CollectionDialog.getUseableLabel(manifest)} </Typography> </DialogTitle> - <ScrollIndicatedDialogContent className={classes.dialogContent}> + <StyledScrollIndicatedDialogContent> { collection && ( <Button startIcon={<ArrowBackIcon />} @@ -197,7 +203,7 @@ export class CollectionDialog extends Component { </Button> )} - <div className={classes.collectionMetadata}> + <StyledCollectionMetadata> <ManifestInfo manifestId={manifest.id} /> <CollapsibleSection id="select-collection-rights" @@ -221,15 +227,15 @@ export class CollectionDialog extends Component { ) } </CollapsibleSection> - </div> - <div className={classes.collectionFilter}> + </StyledCollectionMetadata> + <StyledCollectionFilter> {manifest.getTotalCollections() > 0 && ( <Chip clickable color={currentFilter === 'collections' ? 'primary' : 'default'} onClick={() => this.setFilter('collections')} label={t('totalCollections', { count: manifest.getTotalCollections() })} /> )} {manifest.getTotalManifests() > 0 && ( <Chip clickable color={currentFilter === 'manifests' ? 'primary' : 'default'} onClick={() => this.setFilter('manifests')} label={t('totalManifests', { count: manifest.getTotalManifests() })} /> )} - </div> + </StyledCollectionFilter> { currentFilter === 'collections' && ( <MenuList> { @@ -237,7 +243,7 @@ export class CollectionDialog extends Component { <MenuItem key={c.id} onClick={() => { this.selectCollection(c); }} - className={classes.collectionItem} + variant="multiline" > {CollectionDialog.getUseableLabel(c)} </MenuItem> @@ -252,7 +258,7 @@ export class CollectionDialog extends Component { <MenuItem key={m.id} onClick={() => { this.selectManifest(m); }} - className={classes.collectionItem} + variant="multiline" > {CollectionDialog.getUseableLabel(m)} </MenuItem> @@ -260,7 +266,7 @@ export class CollectionDialog extends Component { } </MenuList> )} - </ScrollIndicatedDialogContent> + </StyledScrollIndicatedDialogContent> <DialogActions> <Button onClick={this.hideDialog}> {t('close')} @@ -273,10 +279,9 @@ export class CollectionDialog extends Component { CollectionDialog.propTypes = { addWindow: PropTypes.func.isRequired, - classes: PropTypes.objectOf(PropTypes.string).isRequired, collection: PropTypes.object, // eslint-disable-line react/forbid-prop-types collectionPath: PropTypes.arrayOf(PropTypes.string), - containerId: PropTypes.string, + container: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), error: PropTypes.string, hideCollectionDialog: PropTypes.func.isRequired, isMultipart: PropTypes.bool, @@ -293,7 +298,7 @@ CollectionDialog.propTypes = { CollectionDialog.defaultProps = { collection: null, collectionPath: [], - containerId: null, + container: null, error: null, isMultipart: false, ready: false, diff --git a/src/components/CollectionInfo.js b/src/components/CollectionInfo.js index 4cd3d3961e5bf804d24687e703199d8b0e6b4d61..ead13a799bbd0b346fa4f1e5b0a4124c395e2700 100644 --- a/src/components/CollectionInfo.js +++ b/src/components/CollectionInfo.js @@ -1,8 +1,8 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import Typography from '@material-ui/core/Typography'; -import ViewListIcon from '@material-ui/icons/ViewListSharp'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import ViewListIcon from '@mui/icons-material/ViewListSharp'; import CollapsibleSection from '../containers/CollapsibleSection'; /** diff --git a/src/components/CompanionArea.js b/src/components/CompanionArea.js index bdf7610b6e5e06f26505d86cce8b4622818ed98b..7d4061ce598a14c05cf87899d8a032411c55e40c 100644 --- a/src/components/CompanionArea.js +++ b/src/components/CompanionArea.js @@ -1,19 +1,57 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Slide from '@material-ui/core/Slide'; -import ArrowLeftIcon from '@material-ui/icons/ArrowLeftSharp'; -import ArrowRightIcon from '@material-ui/icons/ArrowRightSharp'; +import { styled } from '@mui/material/styles'; +import Slide from '@mui/material/Slide'; +import ArrowLeftIcon from '@mui/icons-material/ArrowLeftSharp'; +import ArrowRightIcon from '@mui/icons-material/ArrowRightSharp'; import CompanionWindowFactory from '../containers/CompanionWindowFactory'; import MiradorMenuButton from '../containers/MiradorMenuButton'; import ns from '../config/css-ns'; +const Root = styled('div', { name: 'CompanionArea', slot: 'root' })(({ position, theme }) => ({ + display: 'flex', + minHeight: 0, + position: 'relative', + zIndex: theme.zIndex.appBar - 2, + ...((position === 'bottom' || position === 'far-bottom') && { + flexDirection: 'column', + width: '100%', + }), +})); + +const Container = styled('div', { name: 'CompanionArea', slot: 'container' })(({ ownerState }) => ({ + display: ownerState?.companionAreaOpen ? 'flex' : 'none', + ...((ownerState?.position === 'bottom' || ownerState?.position === 'far-bottom') && { + flexDirection: 'column', + width: '100%', + }), + ...((ownerState?.position === 'left') && { + minWidth: '235px', + }), +})); + +const StyledToggle = styled('div', { name: 'CompanionArea', slot: 'toggle' })(({ theme }) => ({ + alignItems: 'center', + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.mode === 'dark' ? theme.palette.divider : theme.palette.shades?.dark}`, + borderInlineStart: 0, + borderRadius: 0, + display: 'inline-flex', + height: '48px', + left: '100%', + marginTop: '1rem', + overflow: 'hidden', + padding: 2, + position: 'absolute', + width: '23px', + zIndex: theme.zIndex.drawer, +})); + /** */ export class CompanionArea extends Component { /** */ areaLayoutClass() { - const { - classes, position, - } = this.props; + const { classes, position } = this.props; return (position === 'bottom' || position === 'far-bottom') ? classes.horizontal : null; } @@ -51,39 +89,36 @@ export class CompanionArea extends Component { /** */ render() { const { - classes, companionWindowIds, companionAreaOpen, setCompanionAreaOpen, + companionWindowIds, companionAreaOpen, setCompanionAreaOpen, position, sideBarOpen, t, windowId, } = this.props; - + const className = [this.areaLayoutClass(), ns(`companion-area-${position}`)].join(' '); return ( - <div className={[classes.root, this.areaLayoutClass(), ns(`companion-area-${position}`)].join(' ')}> + <Root className={className}> <Slide in={companionAreaOpen} direction={this.slideDirection()}> - <div className={[ns('companion-windows'), companionWindowIds.length > 0 && classes[position], this.areaLayoutClass()].join(' ')} style={{ display: companionAreaOpen ? 'flex' : 'none' }}> - { - companionWindowIds.map(id => ( - <CompanionWindowFactory id={id} key={id} windowId={windowId} /> - )) - } - </div> + <Container + ownerState={this.props} + className={`${ns('companion-windows')} ${companionWindowIds.length > 0}`} + > + {companionWindowIds.map((id) => ( + <CompanionWindowFactory id={id} key={id} windowId={windowId} /> + ))} + </Container> </Slide> - { - setCompanionAreaOpen && position === 'left' && sideBarOpen && companionWindowIds.length > 0 - && ( - <div className={classes.toggle}> - <MiradorMenuButton - aria-expanded={companionAreaOpen} - aria-label={companionAreaOpen ? t('collapseSidePanel') : t('expandSidePanel')} - className={classes.toggleButton} - key={companionAreaOpen ? 'collapse' : 'expand'} - onClick={() => { setCompanionAreaOpen(windowId, !companionAreaOpen); }} - TooltipProps={{ placement: 'right' }} - > - {this.collapseIcon()} - </MiradorMenuButton> - </div> - ) - } - </div> + {setCompanionAreaOpen && position === 'left' && sideBarOpen && companionWindowIds.length > 0 && ( + <StyledToggle> + <MiradorMenuButton + aria-expanded={companionAreaOpen} + aria-label={companionAreaOpen ? t('collapseSidePanel') : t('expandSidePanel')} + edge="start" + onClick={() => { setCompanionAreaOpen(windowId, !companionAreaOpen); }} + TooltipProps={{ placement: 'right' }} + > + {this.collapseIcon()} + </MiradorMenuButton> + </StyledToggle> + )} + </Root> ); } } diff --git a/src/components/CompanionWindow.js b/src/components/CompanionWindow.js index 9cf4fe29a4e55f7576b7b3a6af724b0a87a4297a..8a31ce2fb1b8a4b1127bcdec3e1d9566b7911241 100644 --- a/src/components/CompanionWindow.js +++ b/src/components/CompanionWindow.js @@ -1,15 +1,25 @@ import { Children, cloneElement, Component } from 'react'; import PropTypes from 'prop-types'; -import CloseIcon from '@material-ui/icons/CloseSharp'; -import OpenInNewIcon from '@material-ui/icons/OpenInNewSharp'; -import MoveIcon from '@material-ui/icons/DragIndicatorSharp'; -import Paper from '@material-ui/core/Paper'; -import Typography from '@material-ui/core/Typography'; -import Toolbar from '@material-ui/core/Toolbar'; +import { styled } from '@mui/material/styles'; +import CloseIcon from '@mui/icons-material/CloseSharp'; +import OpenInNewIcon from '@mui/icons-material/OpenInNewSharp'; +import MoveIcon from '@mui/icons-material/DragIndicatorSharp'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import Toolbar from '@mui/material/Toolbar'; import { Rnd } from 'react-rnd'; import MiradorMenuButton from '../containers/MiradorMenuButton'; import ns from '../config/css-ns'; +const Root = styled(Paper, { name: 'CompanionWindow', slot: 'root' })({}); +const StyledToolbar = styled(Toolbar, { name: 'CompanionWindow', slot: 'toolbar' })({}); +const StyledTitle = styled(Typography, { name: 'CompanionWindow', slot: 'title' })({}); +const StyledTitleControls = styled('div', { name: 'CompanionWindow', slot: 'controls' })({}); +const Contents = styled(Paper, { name: 'CompanionWindow', slot: 'contents' })({}); +const StyledRnd = styled(Rnd, { name: 'CompanionWindow', slot: 'resize' })({}); +const StyledPositionButton = styled(MiradorMenuButton, { name: 'CompanionWindow', slot: 'positionButton' })({}); +const StyledCloseButton = styled(MiradorMenuButton, { name: 'CompanionWindow', slot: 'closeButton' })({}); + /** * CompanionWindow */ @@ -69,7 +79,7 @@ export class CompanionWindow extends Component { const { ariaLabel, classes, paperClassName, onCloseClick, updateCompanionWindow, isDisplayed, position, t, title, children, titleControls, size, - defaultSidebarPanelWidth, defaultSidebarPanelHeight, + defaultSidebarPanelWidth, defaultSidebarPanelHeight, innerRef, } = this.props; const isBottom = (position === 'bottom' || position === 'far-bottom'); @@ -87,19 +97,21 @@ export class CompanionWindow extends Component { }); return ( - <Paper - className={[classes.root, position === 'bottom' ? classes.horizontal : classes.vertical, classes[`companionWindow-${position}`], ns(`companion-window-${position}`), paperClassName].join(' ')} + <Root + ownerState={this.props} + ref={innerRef} style={{ display: isDisplayed ? null : 'none', order: position === 'left' ? -1 : null, }} + className={[ns(`companion-window-${position}`), paperClassName, position === 'bottom' ? classes.horizontal : classes.vertical].join(' ')} square component="aside" aria-label={ariaLabel || title} > - <Rnd - className={[classes.rnd]} - style={{ display: 'flex', position: 'relative' }} + <StyledRnd + style={{ display: 'inherit', position: 'inherit' }} + ownerState={this.props} default={{ height: isBottom ? defaultSidebarPanelHeight : '100%', width: isBottom ? 'auto' : defaultSidebarPanelWidth, @@ -110,18 +122,12 @@ export class CompanionWindow extends Component { minWidth={position === 'left' ? 235 : 100} > - <Toolbar - className={[ - classes.toolbar, - classes.companionWindowHeader, - size.width < 370 ? classes.small : null, - ns('companion-window-header'), - ].join(' ')} + <StyledToolbar + variant="dense" + className={[ns('companion-window-header'), size.width < 370 ? classes.small : null].join(' ')} disableGutters > - <Typography variant="h3" className={classes.windowSideBarTitle}> - {title} - </Typography> + <StyledTitle variant="h3">{title}</StyledTitle> { position === 'left' ? updateCompanionWindow @@ -137,49 +143,50 @@ export class CompanionWindow extends Component { <> { updateCompanionWindow && ( - <MiradorMenuButton + <StyledPositionButton aria-label={position === 'bottom' ? t('moveCompanionWindowToRight') : t('moveCompanionWindowToBottom')} - className={classes.positionButton} onClick={() => { updateCompanionWindow({ position: position === 'bottom' ? 'right' : 'bottom' }); }} > <MoveIcon /> - </MiradorMenuButton> + </StyledPositionButton> ) } - <MiradorMenuButton + <StyledCloseButton + sx={{ + ...(size.width < 370 && { + order: 'unset', + }), + }} aria-label={t('closeCompanionWindow')} - className={classes.closeButton} onClick={onCloseClick} > <CloseIcon /> - </MiradorMenuButton> + </StyledCloseButton> </> ) } { titleControls && ( - <div - className={[ - classes.titleControls, - isBottom - ? classes.companionWindowTitleControlsBottom - : classes.companionWindowTitleControls, - ns('companion-window-title-controls'), - ].join(' ')} + <StyledTitleControls + ownerState={{ position }} + sx={{ + order: isBottom || size.width < 370 ? 'unset' : 1000, + }} + className={ns('companion-window-title-controls')} > {titleControls} - </div> + </StyledTitleControls> ) } - </Toolbar> - <Paper - className={[classes.content, ns('scrollto-scrollable')].join(' ')} + </StyledToolbar> + <Contents + className={ns('scrollto-scrollable')} elevation={0} > {childrenWithAdditionalProps} - </Paper> - </Rnd> - </Paper> + </Contents> + </StyledRnd> + </Root> ); } } @@ -187,10 +194,14 @@ export class CompanionWindow extends Component { CompanionWindow.propTypes = { ariaLabel: PropTypes.string, children: PropTypes.node, - classes: PropTypes.objectOf(PropTypes.string).isRequired, + classes: PropTypes.objectOf(PropTypes.string), defaultSidebarPanelHeight: PropTypes.number, defaultSidebarPanelWidth: PropTypes.number, direction: PropTypes.string.isRequired, + innerRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), isDisplayed: PropTypes.bool, onCloseClick: PropTypes.func, paperClassName: PropTypes.string, @@ -208,8 +219,10 @@ CompanionWindow.propTypes = { CompanionWindow.defaultProps = { ariaLabel: undefined, children: undefined, + classes: {}, defaultSidebarPanelHeight: 201, defaultSidebarPanelWidth: 235, + innerRef: undefined, isDisplayed: false, onCloseClick: () => {}, paperClassName: '', diff --git a/src/components/CompanionWindowSection.js b/src/components/CompanionWindowSection.js new file mode 100644 index 0000000000000000000000000000000000000000..312164e85b190960ee6e0adab8b030d3146104fd --- /dev/null +++ b/src/components/CompanionWindowSection.js @@ -0,0 +1,8 @@ +import { styled } from '@mui/material/styles'; + +export const CompanionWindowSection = styled('div', { name: 'CompanionWindowSection', slot: 'root' })(({ theme }) => ({ + paddingBlockEnd: theme.spacing(1), + paddingBlockStart: theme.spacing(2), + paddingInlineEnd: theme.spacing(1), + paddingInlineStart: theme.spacing(2), +})); diff --git a/src/components/ErrorContent.js b/src/components/ErrorContent.js index d6e996ece45f003bfe8bb20b09483454a870409d..b179f769530332094f51158ba1dd664ee310f3c1 100644 --- a/src/components/ErrorContent.js +++ b/src/components/ErrorContent.js @@ -1,19 +1,34 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Accordion from '@material-ui/core/Accordion'; -import AccordionSummary from '@material-ui/core/AccordionSummary'; -import AccordionDetails from '@material-ui/core/AccordionDetails'; -import Typography from '@material-ui/core/Typography'; -import Alert from '@material-ui/lab/Alert'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { styled } from '@mui/material/styles'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import Stack from '@mui/material/Stack'; +import Alert from '@mui/material/Alert'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { PluginHook } from './PluginHook'; +const ErrorStackTrace = styled('pre', { name: 'ErrorContent', slot: 'stacktrace' })({ + overflowY: 'scroll', +}); + +const ErrorMetadata = styled('pre', { name: 'ErrorContent', slot: 'metadata' })({ + height: '100px', + overflowY: 'scroll', +}); + +const InlineAccordion = styled(Accordion, { name: 'ErrorContent', slot: 'accordion' })({ + backgroundColor: 'inherit', + color: 'inherit', + margin: 0, +}); + /** */ export class ErrorContent extends Component { /** */ render() { const { - classes, error, metadata, showJsError, @@ -23,34 +38,28 @@ export class ErrorContent extends Component { if (!showJsError) return null; return ( - <> - <Alert elevation={6} variant="filled" severity="error"> - {t('errorDialogTitle')} - </Alert> - + <Alert elevation={6} variant="filled" severity="error"> + {t('errorDialogTitle')} {showJsError && ( - <Accordion square className={classes.alert}> - <AccordionSummary - expandIcon={<ExpandMoreIcon />} - > - <Typography>{t('jsError', { message: error.message, name: error.name })}</Typography> + <InlineAccordion elevation={2} square> + <AccordionSummary sx={{ marginInlineStart: '-1rem' }} expandIcon={<ExpandMoreIcon sx={{ color: '#fff' }} />}> + {t('jsError', { message: error.message, name: error.name })} </AccordionSummary> - <AccordionDetails className={classes.details}> - <pre>{ t('jsStack', { stack: error.stack }) }</pre> - { metadata && ( - <pre>{JSON.stringify(metadata, null, 2)}</pre> - )} + <AccordionDetails> + <Stack> + <ErrorStackTrace>{t('jsStack', { stack: error.stack })}</ErrorStackTrace> + {metadata && <ErrorMetadata>{JSON.stringify(metadata, null, 2)}</ErrorMetadata>} + </Stack> </AccordionDetails> - </Accordion> + </InlineAccordion> )} <PluginHook {...this.props} /> - </> + </Alert> ); } } ErrorContent.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, error: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types metadata: PropTypes.object, // eslint-disable-line react/forbid-prop-types showJsError: PropTypes.bool, diff --git a/src/components/ErrorDialog.js b/src/components/ErrorDialog.js index 89336efea11eb9f43034a33282bc05f4a1a39ff3..5828a6b98b4f21249f9a0e808945c418f82ce055 100644 --- a/src/components/ErrorDialog.js +++ b/src/components/ErrorDialog.js @@ -1,10 +1,10 @@ import { Component } from 'react'; -import Dialog from '@material-ui/core/Dialog'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogTitle from '@material-ui/core/DialogTitle'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; import PropTypes from 'prop-types'; -import { DialogActions, DialogContentText, Typography } from '@material-ui/core'; -import Button from '@material-ui/core/Button'; +import { DialogActions, DialogContentText } from '@mui/material'; +import Button from '@mui/material/Button'; import { isUndefined } from 'lodash'; /** @@ -28,10 +28,10 @@ export class ErrorDialog extends Component { onClose={() => removeError(error.id)} open={hasError} > - <DialogTitle id="error-dialog-title" disableTypography> - <Typography variant="h2">{t('errorDialogTitle')}</Typography> + <DialogTitle id="error-dialog-title"> + {t('errorDialogTitle')} </DialogTitle> - <DialogContent disableTypography> + <DialogContent> <DialogContentText variant="body2" noWrap color="inherit"> {`${error.message}`} </DialogContentText> diff --git a/src/components/FullScreenButton.js b/src/components/FullScreenButton.js index 3e6b8a8682e1412ca19abbc79a40b534eb6999ea..0aabc6722ef9a9192bfd431c1464a9ea05f7e380 100644 --- a/src/components/FullScreenButton.js +++ b/src/components/FullScreenButton.js @@ -1,6 +1,6 @@ import { Component } from 'react'; -import FullscreenIcon from '@material-ui/icons/FullscreenSharp'; -import FullscreenExitIcon from '@material-ui/icons/FullscreenExitSharp'; +import FullscreenIcon from '@mui/icons-material/FullscreenSharp'; +import FullscreenExitIcon from '@mui/icons-material/FullscreenExitSharp'; import PropTypes from 'prop-types'; import MiradorMenuButton from '../containers/MiradorMenuButton'; import FullScreenContext from '../contexts/FullScreenContext'; @@ -18,7 +18,7 @@ export class FullScreenButton extends Component { } = this.props; return ( <FullScreenContext.Consumer> - { handle => ( + { handle => (handle && ( <MiradorMenuButton className={className} aria-label={handle.active ? t('exitFullScreen') : t('workspaceFullScreen')} @@ -26,7 +26,7 @@ export class FullScreenButton extends Component { > {handle.active ? <FullscreenExitIcon /> : <FullscreenIcon />} </MiradorMenuButton> - )} + ))} </FullScreenContext.Consumer> ); } diff --git a/src/components/GalleryView.js b/src/components/GalleryView.js index 3dc9cfd529178b588350b3b9219db2a11edb748c..1738f56baae0c98c0373d9d84d5e9272ecebccfe 100644 --- a/src/components/GalleryView.js +++ b/src/components/GalleryView.js @@ -1,8 +1,20 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Paper from '@material-ui/core/Paper'; +import { styled } from '@mui/material/styles'; +import Paper from '@mui/material/Paper'; import GalleryViewThumbnail from '../containers/GalleryViewThumbnail'; +const Root = styled(Paper, { name: 'GalleryView', slot: 'root' })(({ theme }) => ({ + alignItems: 'flex-start', + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + overflowX: 'hidden', + overflowY: 'scroll', + padding: '50px 0 50px 20px', + width: '100%', +})); + /** * Renders a GalleryView overview of the manifest. */ @@ -12,16 +24,16 @@ export class GalleryView extends Component { */ render() { const { - canvases, classes, viewingDirection, windowId, + canvases, viewingDirection, windowId, } = this.props; const htmlDir = viewingDirection === 'right-to-left' ? 'rtl' : 'ltr'; return ( - <Paper + <Root component="section" + aria-label="gallery section" dir={htmlDir} square elevation={0} - className={classes.galleryContainer} id={`${windowId}-gallery`} > { @@ -33,19 +45,17 @@ export class GalleryView extends Component { /> )) } - </Paper> + </Root> ); } } GalleryView.propTypes = { canvases: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types - classes: PropTypes.objectOf(PropTypes.string), viewingDirection: PropTypes.string, windowId: PropTypes.string.isRequired, }; GalleryView.defaultProps = { - classes: {}, viewingDirection: '', }; diff --git a/src/components/GalleryViewThumbnail.js b/src/components/GalleryViewThumbnail.js index 961c38af9abebbba2ef05cf3d0b2471d66ccb4b3..efa36629d24ccc1240441ad26f2017e9a55b6948 100644 --- a/src/components/GalleryViewThumbnail.js +++ b/src/components/GalleryViewThumbnail.js @@ -1,14 +1,52 @@ import { createRef, Component } from 'react'; import PropTypes from 'prop-types'; -import Avatar from '@material-ui/core/Avatar'; -import Chip from '@material-ui/core/Chip'; -import AnnotationIcon from '@material-ui/icons/CommentSharp'; -import SearchIcon from '@material-ui/icons/SearchSharp'; -import classNames from 'classnames'; +import { styled } from '@mui/material/styles'; +import Chip from '@mui/material/Chip'; +import AnnotationIcon from '@mui/icons-material/CommentSharp'; +import SearchIcon from '@mui/icons-material/SearchSharp'; import { InView } from 'react-intersection-observer'; -import MiradorCanvas from '../lib/MiradorCanvas'; import IIIFThumbnail from '../containers/IIIFThumbnail'; +const Root = styled('div', { name: 'GalleryView', slot: 'thumbnail' })(({ ownerState, theme }) => ({ + '&:focus': { + outline: 'none', + }, + '&:hover': { + backgroundColor: theme.palette.action.hover, + }, + border: '2px solid transparent', + ...(ownerState.selected && { + borderColor: theme.palette.primary.main, + }), + ...(!ownerState.selected && ownerState.searchAnnotationsCount > 0 && { + borderColor: theme.palette.action.selected, + }), + cursor: 'pointer', + display: 'inline-block', + margin: theme.spacing(1, 0.5), + maxHeight: ownerState.config.height + 45, + minWidth: '60px', + overflow: 'hidden', + padding: theme.spacing(0.5), + position: 'relative', + width: 'min-content', +})); + +const StyledChipsContainer = styled('div', { name: 'GalleryView', slot: 'chipArea' })(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(0.25), + position: 'absolute', + right: 0, + top: 0, +})); + +const AnnotationChip = styled(Chip, { name: 'GalleryView', slot: 'chip' })(({ theme }) => ({ + backgroundColor: theme.palette.annotations.chipBackground, + opacity: 0.875, + textAlign: 'right', +})); + /** * Represents a WindowViewer in the mirador workspace. Responsible for mounting * OSD and Navigation @@ -78,7 +116,7 @@ export class GalleryViewThumbnail extends Component { } /** */ - handleIntersection({ isIntersecting }) { + handleIntersection(_inView, { isIntersecting }) { const { annotationsCount, requestCanvasAnnotations, @@ -102,22 +140,15 @@ export class GalleryViewThumbnail extends Component { render() { const { annotationsCount, searchAnnotationsCount, - canvas, classes, config, selected, + canvas, config, selected, } = this.props; - const miradorCanvas = new MiradorCanvas(canvas); - return ( <InView onChange={this.handleIntersection}> - <div - key={canvas.index} - className={ - classNames( - classes.galleryViewItem, - selected ? classes.selected : '', - searchAnnotationsCount > 0 ? classes.hasAnnotations : '', - ) - } + <Root + ownerState={this.props} + key={canvas.id || canvas.index} + className={selected ? 'selected' : ''} onClick={this.handleSelect} onKeyUp={this.handleKey} ref={this.myRef} @@ -128,45 +159,27 @@ export class GalleryViewThumbnail extends Component { resource={canvas} labelled variant="outside" - maxWidth={config.width} maxHeight={config.height} - style={{ - margin: '0 auto', - maxWidth: `${Math.ceil(config.height * miradorCanvas.aspectRatio)}px`, - }} + maxWidth={config.width} > - <div className={classes.chips}> - { searchAnnotationsCount > 0 && ( - <Chip - avatar={( - <Avatar className={classes.avatar} classes={{ circle: classes.avatarIcon }}> - <SearchIcon fontSize="small" /> - </Avatar> - )} + <StyledChipsContainer> + {searchAnnotationsCount > 0 && ( + <AnnotationChip + icon={<SearchIcon fontSize="small" />} label={searchAnnotationsCount} - className={classNames(classes.searchChip)} size="small" /> )} - { (annotationsCount || 0) > 0 && ( - <Chip - avatar={( - <Avatar className={classes.avatar} classes={{ circle: classes.avatarIcon }}> - <AnnotationIcon className={classes.annotationIcon} /> - </Avatar> - )} + {annotationsCount > 0 && ( + <AnnotationChip + icon={<AnnotationIcon fontSize="small" />} label={annotationsCount} - className={ - classNames( - classes.annotationsChip, - ) - } size="small" /> )} - </div> + </StyledChipsContainer> </IIIFThumbnail> - </div> + </Root> </InView> ); } @@ -175,7 +188,6 @@ export class GalleryViewThumbnail extends Component { GalleryViewThumbnail.propTypes = { annotationsCount: PropTypes.number, canvas: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types - classes: PropTypes.objectOf(PropTypes.string).isRequired, config: PropTypes.shape({ height: PropTypes.number, width: PropTypes.number, diff --git a/src/components/IIIFDropTarget.js b/src/components/IIIFDropTarget.js index 8b48beefc91d35ca4d149023e1f3846f58c4012f..6a15f7123055df530e6f5576d93d48176f3c8697 100644 --- a/src/components/IIIFDropTarget.js +++ b/src/components/IIIFDropTarget.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; -import Backdrop from '@material-ui/core/Backdrop'; -import InsertDriveFileSharpIcon from '@material-ui/icons/InsertDriveFileSharp'; -import { grey } from '@material-ui/core/colors'; +import Backdrop from '@mui/material/Backdrop'; +import InsertDriveFileSharpIcon from '@mui/icons-material/InsertDriveFileSharp'; +import { grey } from '@mui/material/colors'; import { v4 as uuid } from 'uuid'; import { NativeTypes } from 'react-dnd-html5-backend'; import { useDrop } from 'react-dnd'; diff --git a/src/components/IIIFIFrameCommunication.js b/src/components/IIIFIFrameCommunication.js new file mode 100644 index 0000000000000000000000000000000000000000..c26ef42dad4bd4ebed472eac92ced9be865c5bfa --- /dev/null +++ b/src/components/IIIFIFrameCommunication.js @@ -0,0 +1,48 @@ +import { useEffect } from 'react'; +import PropTypes from 'prop-types'; + +/** + * Handle IIIF Auth token validation using iframe message events + */ +export function IIIFIFrameCommunication({ handleReceiveMessage, ...props }) { + // Attaches the 'message' event listener to the window. + useEffect(() => { + if (!handleReceiveMessage) return undefined; + + window.addEventListener('message', handleReceiveMessage); + + // cleanup function + return () => window.removeEventListener('message', handleReceiveMessage, false); + }, [handleReceiveMessage]); + + return ( + // iframe "title" attribute is passed in via props for accessibility + // eslint-disable-next-line jsx-a11y/iframe-has-title + <iframe + {...props} + /> + ); +} + +IIIFIFrameCommunication.propTypes = { + 'aria-hidden': PropTypes.bool, + frameBorder: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + handleReceiveMessage: PropTypes.func, + height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + name: PropTypes.string, + scrolling: PropTypes.string, + src: PropTypes.string.isRequired, + style: PropTypes.shape({}), + width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), +}; + +IIIFIFrameCommunication.defaultProps = { + 'aria-hidden': true, + frameBorder: 0, + handleReceiveMessage: undefined, + height: 1, + name: undefined, + scrolling: undefined, + style: { visibility: 'hidden' }, + width: 1, +}; diff --git a/src/components/IIIFThumbnail.js b/src/components/IIIFThumbnail.js index de2aa4317be1c3886d1b2318063a453fd65779a7..1fd61e66fe9a976cee7a569d17c75eb5f580ccf5 100644 --- a/src/components/IIIFThumbnail.js +++ b/src/components/IIIFThumbnail.js @@ -1,73 +1,61 @@ -import { Component } from 'react'; +import { + Component, useMemo, useEffect, useState, +} from 'react'; import PropTypes from 'prop-types'; -import Typography from '@material-ui/core/Typography'; -import { InView } from 'react-intersection-observer'; -import classNames from 'classnames'; +import { styled } from '@mui/material/styles'; +import { useInView } from 'react-intersection-observer'; import getThumbnail from '../lib/ThumbnailFactory'; -/** - * Uses InteractionObserver to "lazy" load canvas thumbnails that are in view. - */ -export class IIIFThumbnail extends Component { - /** */ - static getUseableLabel(resource, index) { - return (resource - && resource.getLabel - && resource.getLabel().length > 0) - ? resource.getLabel().getValue() - : String(index + 1); - } +const Root = styled('div', { name: 'IIIFThumbnail', slot: 'root' })({}); - /** - */ - constructor(props) { - super(props); - this.state = { loaded: false }; - this.handleIntersection = this.handleIntersection.bind(this); - } +const Label = styled('span', { name: 'IIIFThumbnail', slot: 'label' })(({ theme }) => ({ + ...theme.typography.caption, +})); - /** */ - componentDidMount() { - this.setState(state => ({ ...state, image: this.image() })); - } +const Image = styled('img', { name: 'IIIFThumbnail', slot: 'image' })(() => ({ + height: 'auto', + width: 'auto', +})); - /** */ - componentDidUpdate(prevProps) { - const { maxHeight, maxWidth, resource } = this.props; - - if ( - prevProps.maxHeight !== maxHeight - || prevProps.maxWidth !== maxWidth - || prevProps.resource !== resource) { - this.setState(state => ({ ...state, image: this.image() })); // eslint-disable-line - } - } +/** + * A lazy-loaded image that uses IntersectionObserver to determine when to + * try to load the image (or even calculate that the image url/height/width are) + */ +const LazyLoadedImage = ({ + border, placeholder, style = {}, thumbnail, resource, maxHeight, maxWidth, thumbnailsConfig, ...props +}) => { + const { ref, inView } = useInView(); + const [loaded, setLoaded] = useState(false); /** * Handles the intersection (visibility) of a given thumbnail, by requesting * the image and then updating the state. */ - handleIntersection(inView, _entry) { - const { loaded } = this.state; - + useEffect(() => { if (loaded || !inView) return; - this.setState(state => ({ ...state, loaded: true })); - } + setLoaded(true); + }, [inView, loaded]); - /** - * - */ - imageStyles() { - const { - maxHeight, maxWidth, style, - } = this.props; + const image = useMemo(() => { + if (thumbnail) return thumbnail; - const image = this.image(); + const i = getThumbnail(resource, { ...thumbnailsConfig, maxHeight, maxWidth }); - const styleProps = { height: 'auto', width: 'auto' }; + if (i && i.url) return i; + + return undefined; + }, [resource, thumbnail, maxWidth, maxHeight, thumbnailsConfig]); + + const imageStyles = useMemo(() => { + const styleProps = { + height: undefined, + maxHeight: undefined, + maxWidth: undefined, + width: undefined, + }; - if (!image) return { ...style, height: maxHeight || 'auto', width: maxWidth || 'auto' }; + if (!image) return { ...style, height: maxHeight, width: maxWidth }; const { height: thumbHeight, width: thumbWidth } = image; if (thumbHeight && thumbWidth) { @@ -109,21 +97,56 @@ export class IIIFThumbnail extends Component { ...styleProps, ...style, }; - } - - /** */ - image() { - const { - thumbnail, resource, maxHeight, maxWidth, thumbnailsConfig, - } = this.props; - - if (thumbnail) return thumbnail; + }, [image, maxWidth, maxHeight, style]); + + const { url: src = placeholder } = (loaded && (thumbnail || image)) || {}; + + return ( + <Image + ownerState={{ border }} + ref={ref} + alt="" + role="presentation" + src={src} + style={imageStyles} + {...props} + /> + ); +}; - const image = getThumbnail(resource, { ...thumbnailsConfig, maxHeight, maxWidth }); +LazyLoadedImage.propTypes = { + border: PropTypes.bool, + maxHeight: PropTypes.number.isRequired, + maxWidth: PropTypes.number.isRequired, + placeholder: PropTypes.string.isRequired, + resource: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + style: PropTypes.object, // eslint-disable-line react/forbid-prop-types + thumbnail: PropTypes.shape({ + height: PropTypes.number, + url: PropTypes.string.isRequired, + width: PropTypes.number, + }), + thumbnailsConfig: PropTypes.object, // eslint-disable-line react/forbid-prop-types +}; - if (image && image.url) return image; +LazyLoadedImage.defaultProps = { + border: false, + style: {}, + thumbnail: null, + thumbnailsConfig: {}, +}; - return undefined; +/** + * Uses InteractionObserver to "lazy" load canvas thumbnails that are in view. + */ +export class IIIFThumbnail extends Component { + /** */ + static getUseableLabel(resource, index) { + return (resource + && resource.getLabel + && resource.getLabel().length > 0) + ? resource.getLabel().getValue() + : String(index + 1); } /** */ @@ -137,45 +160,45 @@ export class IIIFThumbnail extends Component { */ render() { const { + border, children, - classes, imagePlaceholder, labelled, + maxHeight, + maxWidth, + resource, + style, thumbnail, - variant, + thumbnailsConfig, } = this.props; - const { image, loaded } = this.state; - - const { url: src = imagePlaceholder } = (loaded && (thumbnail || image)) || {}; - return ( - <div className={classNames(classes.root, { [classes[`${variant}Root`]]: variant })}> - <InView as="span" onChange={this.handleIntersection}> - <img - alt="" - role="presentation" - src={src} - style={this.imageStyles()} - className={classes.image} - /> - </InView> + <Root ownerState={this.props}> + <LazyLoadedImage + placeholder={imagePlaceholder} + thumbnail={thumbnail} + resource={resource} + maxHeight={maxHeight} + maxWidth={maxWidth} + thumbnailsConfig={thumbnailsConfig} + style={style} + border={border} + /> + { labelled && ( - <div className={classNames(classes.label, { [classes[`${variant}Label`]]: variant })}> - <Typography variant="caption" classes={{ root: classNames(classes.caption, { [classes[`${variant}Caption`]]: variant }) }}> - {this.label()} - </Typography> - </div> + <Label ownerState={this.props}> + {this.label()} + </Label> )} {children} - </div> + </Root> ); } } IIIFThumbnail.propTypes = { + border: PropTypes.bool, children: PropTypes.node, - classes: PropTypes.objectOf(PropTypes.string), imagePlaceholder: PropTypes.string, label: PropTypes.string, labelled: PropTypes.bool, @@ -189,12 +212,12 @@ IIIFThumbnail.propTypes = { width: PropTypes.number, }), thumbnailsConfig: PropTypes.object, // eslint-disable-line react/forbid-prop-types - variant: PropTypes.oneOf(['inside', 'outside']), + variant: PropTypes.oneOf(['inside', 'outside']), // eslint-disable-line react/no-unused-prop-types }; IIIFThumbnail.defaultProps = { + border: false, children: null, - classes: {}, // Transparent "gray" imagePlaceholder: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mMMDQmtBwADgwF/Op8FmAAAAABJRU5ErkJggg==', label: undefined, diff --git a/src/components/LabelValueMetadata.js b/src/components/LabelValueMetadata.js index d68650f4099a087a07617112b34e6b9c46cd7bc2..b1fe6c9a29d5d11e9f76a56394d30ace140f5fa1 100644 --- a/src/components/LabelValueMetadata.js +++ b/src/components/LabelValueMetadata.js @@ -1,6 +1,6 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Typography from '@material-ui/core/Typography'; +import Typography from '@mui/material/Typography'; import SanitizedHtml from '../containers/SanitizedHtml'; import ns from '../config/css-ns'; @@ -42,7 +42,7 @@ LabelValueMetadata.propTypes = { defaultLabel: PropTypes.string, labelValuePairs: PropTypes.arrayOf(PropTypes.shape({ label: PropTypes.string, - value: PropTypes.string, + values: PropTypes.arrayOf(PropTypes.string), })).isRequired, }; diff --git a/src/components/LanguageSettings.js b/src/components/LanguageSettings.js index 7901c4e440ada6bd9e5f251b71998158a850ed6a..f0048c4788abbc9b9ef50d49818dbd40cf4f7456 100644 --- a/src/components/LanguageSettings.js +++ b/src/components/LanguageSettings.js @@ -1,8 +1,8 @@ import { Component } from 'react'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import ListItemText from '@material-ui/core/ListItemText'; -import MenuItem from '@material-ui/core/MenuItem'; -import CheckIcon from '@material-ui/icons/CheckSharp'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import MenuItem from '@mui/material/MenuItem'; +import CheckIcon from '@mui/icons-material/CheckSharp'; import PropTypes from 'prop-types'; /** @@ -23,7 +23,6 @@ export class LanguageSettings extends Component { { languages.map(language => ( <MenuItem - button={!language.current} key={language.locale} onClick={() => { handleClick(language.locale); }} > diff --git a/src/components/LocalePicker.js b/src/components/LocalePicker.js index beb212324eb40595e03bde77284bd7a2e3303636..a1f9d8593dea7ff5e7751ea8e583951859c4f22a 100644 --- a/src/components/LocalePicker.js +++ b/src/components/LocalePicker.js @@ -1,9 +1,9 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import MenuItem from '@material-ui/core/MenuItem'; -import FormControl from '@material-ui/core/FormControl'; -import Select from '@material-ui/core/Select'; -import Typography from '@material-ui/core/Typography'; +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import Select from '@mui/material/Select'; +import Typography from '@mui/material/Typography'; /** * Provide a locale picker @@ -16,7 +16,6 @@ export class LocalePicker extends Component { render() { const { availableLocales, - classes, locale, setLocale, } = this.props; @@ -30,14 +29,11 @@ export class LocalePicker extends Component { horizontal: 'left', vertical: 'bottom', }, - getContentAnchorEl: null, }} displayEmpty value={locale} onChange={(e) => { setLocale(e.target.value); }} name="locale" - classes={{ select: classes.select }} - className={classes.selectEmpty} > { availableLocales.map(l => ( @@ -52,14 +48,12 @@ export class LocalePicker extends Component { LocalePicker.propTypes = { availableLocales: PropTypes.arrayOf(PropTypes.string), - classes: PropTypes.objectOf(PropTypes.string), locale: PropTypes.string, setLocale: PropTypes.func, }; LocalePicker.defaultProps = { availableLocales: [], - classes: {}, locale: '', setLocale: undefined, }; diff --git a/src/components/ManifestForm.js b/src/components/ManifestForm.js index 18d304cd6dca0f1301e1dd735d812d9135641eb8..2266db77c9df80ab685463034e7e9872117c2868 100644 --- a/src/components/ManifestForm.js +++ b/src/components/ManifestForm.js @@ -1,8 +1,8 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import Grid from '@material-ui/core/Grid'; -import TextField from '@material-ui/core/TextField'; +import Button from '@mui/material/Button'; +import Grid from '@mui/material/Grid'; +import TextField from '@mui/material/TextField'; /** * Provides a form for user input of a manifest url @@ -68,7 +68,6 @@ export class ManifestForm extends Component { const { formValue } = this.state; const { addResourcesOpen, - classes, onCancel, t, } = this.props; @@ -92,11 +91,19 @@ export class ManifestForm extends Component { shrink: true, }} InputProps={{ - className: classes.input, + style: { typography: 'body1' }, }} /> </Grid> - <Grid item xs={12} sm={4} md={3} className={classes.buttons}> + <Grid + item + xs={12} + sm={4} + md={3} + sx={{ + textAlign: { sm: 'inherit', xs: 'right' }, + }} + > { onCancel && ( <Button onClick={this.handleCancel}> {t('cancel')} @@ -115,14 +122,12 @@ export class ManifestForm extends Component { ManifestForm.propTypes = { addResource: PropTypes.func.isRequired, addResourcesOpen: PropTypes.bool.isRequired, - classes: PropTypes.objectOf(PropTypes.string), onCancel: PropTypes.func, onSubmit: PropTypes.func, t: PropTypes.func, }; ManifestForm.defaultProps = { - classes: {}, onCancel: null, onSubmit: () => {}, t: key => key, diff --git a/src/components/ManifestInfo.js b/src/components/ManifestInfo.js index a66be288bc11fc2d1198b1f1e144169ab7136791..e465a054da023abe4245147b934ff4759aef2918 100644 --- a/src/components/ManifestInfo.js +++ b/src/components/ManifestInfo.js @@ -1,6 +1,6 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Typography from '@material-ui/core/Typography'; +import Typography from '@mui/material/Typography'; import CollapsibleSection from '../containers/CollapsibleSection'; import SanitizedHtml from '../containers/SanitizedHtml'; import { LabelValueMetadata } from './LabelValueMetadata'; @@ -17,6 +17,7 @@ export class ManifestInfo extends Component { render() { const { manifestDescription, + manifestSummary, manifestLabel, manifestMetadata, id, @@ -45,6 +46,12 @@ export class ManifestInfo extends Component { </Typography> )} + {manifestSummary && ( + <Typography variant="body1"> + <SanitizedHtml htmlString={manifestSummary} ruleSet="iiif" /> + </Typography> + )} + {manifestMetadata.length > 0 && ( <LabelValueMetadata labelValuePairs={manifestMetadata} /> )} @@ -60,6 +67,7 @@ ManifestInfo.propTypes = { manifestDescription: PropTypes.string, manifestLabel: PropTypes.string, manifestMetadata: PropTypes.array, // eslint-disable-line react/forbid-prop-types + manifestSummary: PropTypes.string, t: PropTypes.func, }; @@ -67,5 +75,6 @@ ManifestInfo.defaultProps = { manifestDescription: null, manifestLabel: null, manifestMetadata: [], + manifestSummary: null, t: key => key, }; diff --git a/src/components/ManifestListItem.js b/src/components/ManifestListItem.js index accd5ede56d5dadcfb6ab8d791359aaeaf838b13..18e26fbc921ecb673c5afc94870e2e7e214ea555 100644 --- a/src/components/ManifestListItem.js +++ b/src/components/ManifestListItem.js @@ -1,14 +1,42 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import ListItem from '@material-ui/core/ListItem'; -import ButtonBase from '@material-ui/core/ButtonBase'; -import Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; -import Skeleton from '@material-ui/lab/Skeleton'; +import { styled } from '@mui/material/styles'; +import ListItem from '@mui/material/ListItem'; +import ButtonBase from '@mui/material/ButtonBase'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; +import Skeleton from '@mui/material/Skeleton'; import { Img } from 'react-image'; import ManifestListItemError from '../containers/ManifestListItemError'; import ns from '../config/css-ns'; +const Root = styled(ListItem, { name: 'ManifestListItem', slot: 'root' })(({ ownerState, theme }) => ({ + '&:hover,&:focus-within': { + backgroundColor: theme.palette.action.hover, + borderLeftColor: ownerState?.active ? theme.palette.primary.main : theme.palette.action.hover, + }, + borderLeft: '4px solid', + borderLeftColor: ownerState?.active ? theme.palette.primary.main : 'transparent', + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + [theme.breakpoints.up('sm')]: { + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3), + }, +})); + +const StyledThumbnail = styled(Img, { name: 'ManifestListItem', slot: 'thumbnail' })(({ theme }) => ({ + maxWidth: '100%', + objectFit: 'contain', +})); + +const StyledLogo = styled(Img, { name: 'ManifestListItem', slot: 'logo' })(({ theme }) => ({ + height: '2.5rem', + maxWidth: '100%', + objectFit: 'contain', + paddingRight: 1, +})); + /** * Represents an item in a list of currently-loaded or loading manifests * @param {object} props @@ -57,7 +85,6 @@ export class ManifestListItem extends Component { thumbnail, manifestLogo, size, - classes, provider, t, error, @@ -68,61 +95,81 @@ export class ManifestListItem extends Component { const placeholder = ( <Grid container className={ns('manifest-list-item')} spacing={2}> <Grid item xs={3} sm={2}> - <Skeleton className={classes.placeholder} variant="rect" height={80} width={120} /> + <Skeleton sx={{ bgcolor: 'grey[300]' }} variant="rectangular" height={80} width={120} /> </Grid> <Grid item xs={9} sm={6}> - <Skeleton className={classes.placeholder} variant="text" /> + <Skeleton sx={{ bgcolor: 'grey[300]' }} variant="text" /> </Grid> <Grid item xs={8} sm={2}> - <Skeleton className={classes.placeholder} variant="text" /> - <Skeleton className={classes.placeholder} variant="text" /> + <Skeleton sx={{ bgcolor: 'grey[300]' }} variant="text" /> + <Skeleton sx={{ bgcolor: 'grey[300]' }} variant="text" /> </Grid> <Grid item xs={4} sm={2}> - <Skeleton className={classes.placeholder} variant="rect" height={60} width={60} /> + <Skeleton sx={{ bgcolor: 'grey[300]' }} variant="rectangular" height={60} width={60} /> </Grid> </Grid> ); if (error) { return ( - <ListItem divider className={classes.root} data-manifestid={manifestId}> + <Root + ownerState={this.props} + divider + selected={active} + className={active ? 'active' : ''} + data-manifestid={manifestId} + > <ManifestListItemError manifestId={manifestId} /> - </ListItem> + </Root> ); } return ( - <ListItem divider className={[classes.root, active ? classes.active : ''].join(' ')} data-manifestid={manifestId}> + <Root + divider + selected={active} + className={active ? 'active' : ''} + data-manifestid={manifestId} + data-active={active} + > {ready ? ( <Grid container className={ns('manifest-list-item')} spacing={2}> - <Grid item xs={12} sm={6} className={classes.buttonGrid}> + <Grid item xs={12} sm={6}> <ButtonBase ref={buttonRef} className={ns('manifest-list-item-title')} style={{ width: '100%' }} onClick={this.handleOpenButtonClick} > - <Grid container spacing={2} className={classes.label} component="span"> + <Grid + container + spacing={2} + sx={{ + textAlign: 'left', + textTransform: 'initial', + }} + component="span" + > <Grid item xs={4} sm={3} component="span"> { thumbnail ? ( - <Img - className={[classes.thumbnail, ns('manifest-list-item-thumb')].join(' ')} + <StyledThumbnail + className={[ns('manifest-list-item-thumb')]} src={[thumbnail]} alt="" height="80" unloader={( <Skeleton - variant="rect" + variant="rectangular" animation={false} - className={classes.placeholder} + sx={{ bgcolor: 'grey[300]' }} height={80} width={120} /> )} /> ) - : <Skeleton className={classes.placeholder} variant="rect" height={80} width={120} />} + : <Skeleton sx={{ bgcolor: 'grey[300]' }} variant="rectangular" height={80} width={120} />} </Grid> <Grid item xs={8} sm={9} component="span"> { isCollection && ( @@ -145,16 +192,15 @@ export class ManifestListItem extends Component { <Grid item xs={4} sm={2}> { manifestLogo && ( - <Img + <StyledLogo src={[manifestLogo]} alt="" role="presentation" - className={classes.logo} unloader={( <Skeleton - variant="rect" + variant="rectangular" animation={false} - className={classes.placeholder} + sx={{ bgcolor: 'grey[300]' }} height={60} width={60} /> @@ -166,7 +212,7 @@ export class ManifestListItem extends Component { ) : ( placeholder )} - </ListItem> + </Root> ); } } @@ -175,7 +221,6 @@ ManifestListItem.propTypes = { active: PropTypes.bool, addWindow: PropTypes.func.isRequired, buttonRef: PropTypes.elementType, - classes: PropTypes.objectOf(PropTypes.string), error: PropTypes.string, fetchManifest: PropTypes.func.isRequired, handleClose: PropTypes.func, @@ -195,7 +240,6 @@ ManifestListItem.propTypes = { ManifestListItem.defaultProps = { active: false, buttonRef: undefined, - classes: {}, error: null, handleClose: () => {}, isCollection: false, diff --git a/src/components/ManifestListItemError.js b/src/components/ManifestListItemError.js index 3d63e48afd3445ff3df92814908d91c9ca19a1d2..b8d5176aa5aaa6a967b432f2af601f48fcd67268 100644 --- a/src/components/ManifestListItemError.js +++ b/src/components/ManifestListItemError.js @@ -1,9 +1,9 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import ErrorIcon from '@material-ui/icons/ErrorOutlineSharp'; -import Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; +import Button from '@mui/material/Button'; +import ErrorIcon from '@mui/icons-material/ErrorOutlineSharp'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; /** * ManifestListItemError renders a component displaying a @@ -15,7 +15,7 @@ export class ManifestListItemError extends Component { */ render() { const { - classes, manifestId, onDismissClick, onTryAgainClick, t, + manifestId, onDismissClick, onTryAgainClick, t, } = this.props; return ( @@ -24,12 +24,17 @@ export class ManifestListItemError extends Component { <Grid container item xs={12} sm={6}> <Grid item xs={4} sm={3}> <Grid container justifyContent="center"> - <ErrorIcon className={classes.errorIcon} /> + <ErrorIcon sx={{ + color: 'error.main', + height: '2rem', + width: '2rem', + }} + /> </Grid> </Grid> <Grid item xs={8} sm={9}> <Typography>{t('manifestError')}</Typography> - <Typography className={classes.manifestIdText}>{manifestId}</Typography> + <Typography sx={{ wordBreak: 'break-all' }}>{manifestId}</Typography> </Grid> </Grid> </Grid> @@ -52,7 +57,6 @@ export class ManifestListItemError extends Component { } ManifestListItemError.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, manifestId: PropTypes.string.isRequired, onDismissClick: PropTypes.func.isRequired, onTryAgainClick: PropTypes.func.isRequired, diff --git a/src/components/ManifestRelatedLinks.js b/src/components/ManifestRelatedLinks.js index 62436ea79a4ba2009ff7ae9cee421a83e1995d40..cbba6aefd1defc8c2a02ab6f68a6b459a27d2ec6 100644 --- a/src/components/ManifestRelatedLinks.js +++ b/src/components/ManifestRelatedLinks.js @@ -1,12 +1,19 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Typography from '@material-ui/core/Typography'; -import Link from '@material-ui/core/Link'; +import { styled } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; +import Link from '@mui/material/Link'; import classNames from 'classnames'; import CollapsibleSection from '../containers/CollapsibleSection'; import ns from '../config/css-ns'; import { PluginHook } from './PluginHook'; +const StyledDl = styled('dl')(({ theme }) => ({ + '& dd': { + marginBottom: '.5em', + marginLeft: '0', + }, +})); /** * ManifestRelatedLinks */ @@ -17,7 +24,6 @@ export class ManifestRelatedLinks extends Component { */ render() { const { - classes, homepage, manifestUrl, related, @@ -40,7 +46,7 @@ export class ManifestRelatedLinks extends Component { > {t('links')} </Typography> - <dl className={classNames(ns('label-value-metadata'), classes.labelValueMetadata)}> + <StyledDl className={classNames(ns('label-value-metadata'))}> { homepage && ( <> <Typography variant="subtitle2" component="dt">{t('iiif_homepage')}</Typography> @@ -113,7 +119,7 @@ export class ManifestRelatedLinks extends Component { </Typography> </> )} - </dl> + </StyledDl> <PluginHook {...this.props} /> </CollapsibleSection> ); @@ -121,7 +127,6 @@ export class ManifestRelatedLinks extends Component { } ManifestRelatedLinks.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, homepage: PropTypes.arrayOf(PropTypes.shape({ label: PropTypes.string, value: PropTypes.string, diff --git a/src/components/MinimalWindow.js b/src/components/MinimalWindow.js index f19b35385f339bf38761b7d6390e421101833330..b04661692437238242a47ea242933c80c82c0bfe 100644 --- a/src/components/MinimalWindow.js +++ b/src/components/MinimalWindow.js @@ -1,15 +1,19 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import MenuIcon from '@material-ui/icons/MenuSharp'; +import { styled } from '@mui/material/styles'; +import MenuIcon from '@mui/icons-material/MenuSharp'; import cn from 'classnames'; -import Paper from '@material-ui/core/Paper'; -import AppBar from '@material-ui/core/AppBar'; -import Toolbar from '@material-ui/core/Toolbar'; -import Typography from '@material-ui/core/Typography'; -import CloseIcon from '@material-ui/icons/CloseSharp'; +import Paper from '@mui/material/Paper'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import CloseIcon from '@mui/icons-material/CloseSharp'; import MiradorMenuButton from '../containers/MiradorMenuButton'; import ns from '../config/css-ns'; +const StyledMiradorMenuButton = styled(MiradorMenuButton)(() => ({ + marginLeft: 'auto', +})); /** */ export class MinimalWindow extends Component { /** */ @@ -19,7 +23,6 @@ export class MinimalWindow extends Component { allowWindowSideBar, ariaLabel, children, - classes, label, removeWindow, t, @@ -32,17 +35,31 @@ export class MinimalWindow extends Component { elevation={1} id={windowId} className={ - cn(classes.window, ns('placeholder-window')) + cn(ns('placeholder-window')) } + sx={{ + backgroundColor: 'shades.dark', + borderRadius: 0, + display: 'flex', + flexDirection: 'column', + height: '100%', + minHeight: 0, + overflow: 'hidden', + width: '100%', + }} aria-label={label && ariaLabel ? t('window', { label }) : null} > - <AppBar position="relative" color="default"> + <AppBar position="relative" color="default" enableColorOnDark> <Toolbar disableGutters - className={cn( - classes.windowTopBarStyle, - ns('window-top-bar'), - )} + className={cn(ns('window-top-bar'))} + sx={{ + backgroundColor: 'shades.main', + borderTop: '2px solid transparent', + minHeight: 32, + paddingLeft: 0.5, + paddingRight: 0.5, + }} variant="dense" > {allowWindowSideBar && ( @@ -53,20 +70,29 @@ export class MinimalWindow extends Component { <MenuIcon /> </MiradorMenuButton> )} - <Typography variant="h2" noWrap color="inherit" className={classes.title}> + <Typography + variant="h2" + noWrap + color="inherit" + sx={{ + flexGrow: 1, + paddingLeft: 0.5, + typography: 'h6', + }} + > {label} </Typography> {allowClose && removeWindow && ( - <MiradorMenuButton + <StyledMiradorMenuButton aria-label={t('closeWindow')} - className={cn(classes.button, ns('window-close'))} + className={cn(ns('window-close'))} onClick={removeWindow} TooltipProps={{ - tabIndex: ariaLabel ? '0' : '-1', + tabIndex: ariaLabel ? 0 : -1, }} > <CloseIcon /> - </MiradorMenuButton> + </StyledMiradorMenuButton> )} </Toolbar> </AppBar> @@ -81,7 +107,6 @@ MinimalWindow.propTypes = { allowWindowSideBar: PropTypes.bool, ariaLabel: PropTypes.bool, children: PropTypes.node, - classes: PropTypes.objectOf(PropTypes.string), label: PropTypes.string, removeWindow: PropTypes.func, t: PropTypes.func, @@ -93,7 +118,6 @@ MinimalWindow.defaultProps = { allowWindowSideBar: true, ariaLabel: true, children: null, - classes: {}, label: '', removeWindow: () => {}, t: key => key, diff --git a/src/components/MiradorMenuButton.js b/src/components/MiradorMenuButton.js index d528a7aef21fb65575e5b62e48abd986fe3364e0..e1def5a7b87b9283121bacf3e8a85e04da4e6b2f 100644 --- a/src/components/MiradorMenuButton.js +++ b/src/components/MiradorMenuButton.js @@ -1,8 +1,15 @@ import PropTypes from 'prop-types'; -import Badge from '@material-ui/core/Badge'; -import IconButton from '@material-ui/core/IconButton'; -import Tooltip from '@material-ui/core/Tooltip'; -import ns from '../config/css-ns'; +import { styled } from '@mui/material/styles'; +import Badge from '@mui/material/Badge'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; + +const Root = styled(IconButton, { name: 'MiradorMenuButton', slot: 'root' })(({ ownerState, theme }) => ({ + fill: 'currentcolor', + ...(ownerState.selected && { + backgroundColor: theme.palette.action.selected, + }), +})); /** * MiradorMenuButton ~ Wrap the given icon prop in an IconButton and a Tooltip. @@ -14,19 +21,32 @@ export function MiradorMenuButton(props) { const { badge, children, - containerId, + container, dispatch, BadgeProps, TooltipProps, + sx, ...iconButtonProps } = props; const button = ( - <IconButton {...iconButtonProps}> + <Root + ownerState={props} + {...iconButtonProps} + sx={sx} + size="large" + > {badge - ? <Badge {...BadgeProps}>{children}</Badge> + ? ( + <Badge + overlap="rectangular" + {...BadgeProps} + > + {children} + </Badge> + ) : children} - </IconButton> + </Root> ); if (iconButtonProps.disabled) return button; @@ -34,7 +54,7 @@ export function MiradorMenuButton(props) { return ( <Tooltip PopperProps={{ - container: document.querySelector(`#${containerId} .${ns('viewer')}`), + container: container?.current, }} title={ariaLabel} {...TooltipProps} @@ -49,14 +69,19 @@ MiradorMenuButton.propTypes = { badge: PropTypes.bool, BadgeProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types children: PropTypes.element.isRequired, - containerId: PropTypes.string.isRequired, + container: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), dispatch: PropTypes.func, + selected: PropTypes.bool, + sx: PropTypes.object, // eslint-disable-line react/forbid-prop-types TooltipProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types }; MiradorMenuButton.defaultProps = { badge: false, BadgeProps: {}, + container: null, dispatch: () => {}, + selected: false, + sx: {}, TooltipProps: {}, }; diff --git a/src/components/NestedMenu.js b/src/components/NestedMenu.js index 65fcfeec57a1d7681510747b3dc3bb120358d57f..b61107786453d1b98aff1749f704089d9e686e10 100644 --- a/src/components/NestedMenu.js +++ b/src/components/NestedMenu.js @@ -1,10 +1,10 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import ListItemText from '@material-ui/core/ListItemText'; -import MenuItem from '@material-ui/core/MenuItem'; -import ExpandLess from '@material-ui/icons/ExpandLessSharp'; -import ExpandMore from '@material-ui/icons/ExpandMoreSharp'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import MenuItem from '@mui/material/MenuItem'; +import ExpandLess from '@mui/icons-material/ExpandLessSharp'; +import ExpandMore from '@mui/icons-material/ExpandMoreSharp'; /** * NestedMenu ~ A presentation component to render a menu item and have diff --git a/src/components/OpenSeadragonViewer.js b/src/components/OpenSeadragonViewer.js index 5103f69941175629d9707e00cf0bc4a4541a2ff7..120aba5151fee7995e5985902cf3e8cf8bcdcc84 100644 --- a/src/components/OpenSeadragonViewer.js +++ b/src/components/OpenSeadragonViewer.js @@ -2,6 +2,7 @@ import { createRef, Children, cloneElement, Component, } from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; import debounce from 'lodash/debounce'; import isEqual from 'lodash/isEqual'; import OpenSeadragon from 'openseadragon'; @@ -12,6 +13,11 @@ import CanvasWorld from '../lib/CanvasWorld'; import { PluginHook } from './PluginHook'; import { OSDReferences } from '../plugins/OSDReferences'; +const StyledSection = styled('section')({ + cursor: 'grab', + flex: 1, + position: 'relative', +}); /** * Represents a OpenSeadragonViewer in the mirador workspace. Responsible for mounting * and rendering OSD. @@ -23,7 +29,7 @@ export class OpenSeadragonViewer extends Component { constructor(props) { super(props); - this.state = { viewer: undefined }; + this.state = { grabbing: false, viewer: undefined }; this.ref = createRef(); this.apiRef = createRef(); OSDReferences.set(props.windowId, this.apiRef); @@ -58,6 +64,14 @@ export class OpenSeadragonViewer extends Component { this.setState({ viewer }); + viewer.addHandler('canvas-drag', () => { + this.setState({ grabbing: true }); + }); + + viewer.addHandler('canvas-drag-end', () => { + this.setState({ grabbing: false }); + }); + // Set a flag when OSD starts animating (so that viewer updates are not used) viewer.addHandler('animation-start', () => { this.osdUpdating = true; @@ -139,6 +153,7 @@ export class OpenSeadragonViewer extends Component { viewer.innerTracker.moveHandler = null; } viewer.removeAllHandlers(); + this.onCanvasMouseMove.cancel(); this.apiRef.current = undefined; } @@ -192,10 +207,6 @@ export class OpenSeadragonViewer extends Component { if (!(type === 'Image' || type === 'dctypes:Image' || format.startsWith('image/'))) return Promise.resolve(); return new Promise((resolve, reject) => { - if (!viewer) { - reject(); - } - resolve(viewer.addSimpleImage({ error: event => reject(event), fitBounds: new OpenSeadragon.Rect( @@ -215,10 +226,6 @@ export class OpenSeadragonViewer extends Component { const { canvasWorld } = this.props; const { viewer } = this.state; return new Promise((resolve, reject) => { - if (!viewer) { - reject(); - } - // OSD mutates this object, so we give it a shallow copy const tileSource = { ...infoResponse.json }; const contentResource = canvasWorld.contentResource(infoResponse.id); @@ -242,9 +249,6 @@ export class OpenSeadragonViewer extends Component { refreshTileProperties() { const { canvasWorld } = this.props; const { viewer } = this.state; - - if (!viewer) return; - const { world } = viewer; const items = []; @@ -342,10 +346,10 @@ export class OpenSeadragonViewer extends Component { */ render() { const { - children, classes, label, t, windowId, + children, label, t, windowId, drawAnnotations, } = this.props; - const { viewer } = this.state; + const { viewer, grabbing } = this.state; const enhancedChildren = Children.map(children, child => ( cloneElement( @@ -357,8 +361,9 @@ export class OpenSeadragonViewer extends Component { )); return ( - <section - className={classNames(ns('osd-container'), classes.osdContainer)} + <StyledSection + className={classNames(ns('osd-container'))} + style={{ cursor: grabbing ? 'grabbing' : undefined }} id={`${windowId}-osd`} ref={this.ref} aria-label={t('item', { label })} @@ -368,7 +373,7 @@ export class OpenSeadragonViewer extends Component { && <AnnotationsOverlay viewer={viewer} windowId={windowId} /> } { enhancedChildren } <PluginHook viewer={viewer} {...{ ...this.props, children: null }} /> - </section> + </StyledSection> ); } } @@ -386,7 +391,6 @@ OpenSeadragonViewer.defaultProps = { OpenSeadragonViewer.propTypes = { canvasWorld: PropTypes.instanceOf(CanvasWorld).isRequired, children: PropTypes.node, - classes: PropTypes.objectOf(PropTypes.string).isRequired, drawAnnotations: PropTypes.bool, infoResponses: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types label: PropTypes.string, diff --git a/src/components/PrimaryWindow.js b/src/components/PrimaryWindow.js index 988b6784aea40a2167782f35ea06cc8964e10851..4d6344b3acb3f692e6a0511697751016ebeadb03 100644 --- a/src/components/PrimaryWindow.js +++ b/src/components/PrimaryWindow.js @@ -1,6 +1,6 @@ import { Component, lazy, Suspense } from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; +import { styled } from '@mui/material/styles'; import WindowSideBar from '../containers/WindowSideBar'; import CompanionArea from '../containers/CompanionArea'; import CollectionDialog from '../containers/CollectionDialog'; @@ -16,6 +16,12 @@ GalleryView.displayName = 'GalleryView'; SelectCollection.displayName = 'SelectCollection'; WindowViewer.displayName = 'WindowViewer'; +const StyledPrimaryWindowContainer = styled('div')(() => ({ + display: 'flex', + flex: 1, + position: 'relative', +})); + /** * PrimaryWindow - component that renders the primary content of a Mirador * window. Right now this differentiates between a Image, Video, or Audio viewer. @@ -74,17 +80,17 @@ export class PrimaryWindow extends Component { */ render() { const { - isCollectionDialogVisible, windowId, classes, children, + isCollectionDialogVisible, windowId, children, } = this.props; return ( - <div className={classNames(ns('primary-window'), classes.primaryWindow)}> + <StyledPrimaryWindowContainer data-testid="test-window" className={ns('primary-window')}> <WindowSideBar windowId={windowId} /> <CompanionArea windowId={windowId} position="left" /> { isCollectionDialogVisible && <CollectionDialog windowId={windowId} /> } <Suspense fallback={<div />}> {children || this.renderViewer()} </Suspense> - </div> + </StyledPrimaryWindowContainer> ); } } @@ -92,7 +98,6 @@ export class PrimaryWindow extends Component { PrimaryWindow.propTypes = { audioResources: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types children: PropTypes.node, - classes: PropTypes.objectOf(PropTypes.string).isRequired, isCollection: PropTypes.bool, isCollectionDialogVisible: PropTypes.bool, isFetching: PropTypes.bool, diff --git a/src/components/SanitizedHtml.js b/src/components/SanitizedHtml.js index da9bcd7021be52ae46f56f35c1027fc690c51946..dcbab42e3b20ca80f030dd607c9f5639d4658cb3 100644 --- a/src/components/SanitizedHtml.js +++ b/src/components/SanitizedHtml.js @@ -1,16 +1,21 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; import DOMPurify from 'dompurify'; import ns from '../config/css-ns'; import htmlRules from '../lib/htmlRules'; +const Root = styled('span', { name: 'IIIFHtmlContent', slot: 'root' })({}); + /** */ export class SanitizedHtml extends Component { /** */ render() { - const { classes, htmlString, ruleSet } = this.props; + const { + classes, htmlString, ruleSet, ...props + } = this.props; // Add a hook to make all links open a new window DOMPurify.addHook('afterSanitizeAttributes', (node) => { @@ -23,11 +28,12 @@ export class SanitizedHtml extends Component { }); return ( - <span - className={[classes.root, ns('third-party-html')].join(' ')} + <Root + className={[ns('third-party-html'), classes.root].join(' ')} dangerouslySetInnerHTML={{ // eslint-disable-line react/no-danger __html: DOMPurify.sanitize(htmlString, htmlRules[ruleSet]), }} + {...props} /> ); } diff --git a/src/components/ScrollIndicatedDialogContent.js b/src/components/ScrollIndicatedDialogContent.js index a8de5229d8d8e09e70aa881432246ccb568c5fc3..01e10ca255355b32e255b4f3380d0bfa6e814fb5 100644 --- a/src/components/ScrollIndicatedDialogContent.js +++ b/src/components/ScrollIndicatedDialogContent.js @@ -1,5 +1,49 @@ import PropTypes from 'prop-types'; -import DialogContent from '@material-ui/core/DialogContent'; +import DialogContent from '@mui/material/DialogContent'; +import { alpha, styled } from '@mui/material/styles'; + +/** + * From https://github.com/mui/material-ui/blob/v5.15.0/packages/mui-material/src/styles/getOverlayAlpha.ts + */ +const getOverlayAlpha = (elevation) => { + let alphaValue; + if (elevation < 1) { + alphaValue = 5.11916 * elevation ** 2; + } else { + alphaValue = 4.5 * Math.log(elevation + 1) + 2; + } + return (alphaValue / 100).toFixed(2); +}; + +const Root = styled(DialogContent, { name: 'ScrollIndicatedDialogContent', slot: 'root' })(({ ownerState, theme }) => { + // In dark mode, paper has a elevation-dependent background color: + // https://github.com/mui/material-ui/blob/v5.15.0/packages/mui-material/src/Paper/Paper.js#L55-L60 + const bgcolor = theme.palette.mode === 'dark' ? { + backgroundImage: `linear-gradient(${alpha( + '#fff', + getOverlayAlpha(ownerState?.elevation || 24), + )}, ${alpha('#fff', getOverlayAlpha(ownerState?.elevation || 24))})`, + } : theme.palette.background.paper; + return { + /* Shadow covers */ + background: `linear-gradient(${bgcolor} 30%, rgba(255, 255, 255, 0)), ` + + `linear-gradient(rgba(255, 255, 255, 0), ${bgcolor} 70%) 0 100%, ` + // Shaddows + + 'radial-gradient(50% 0, farthest-side, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)), ' + + 'radial-gradient(50% 100%, farthest-side, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)) 0 100%,', + /* Shadow covers */ + background: `linear-gradient(${bgcolor} 30%, rgba(255, 255, 255, 0)), ` // eslint-disable-line no-dupe-keys + + `linear-gradient(rgba(255, 255, 255, 0), ${bgcolor} 70%) 0 100%, ` + // Shaddows + + 'radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)), ' + + 'radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)) 0 100%;', + + backgroundAttachment: 'local, local, scroll, scroll', + backgroundRepeat: 'no-repeat', + backgroundSize: '100% 40px, 100% 40px, 100% 14px, 100% 14px', + overflowY: 'auto', + }; +}); /** * ScrollIndicatedDialogContent ~ Inject a style into the DialogContent component @@ -10,18 +54,21 @@ export function ScrollIndicatedDialogContent(props) { const ourClassName = [className, classes.shadowScrollDialog].join(' '); return ( - <DialogContent className={ourClassName} {...otherProps} /> + <Root + className={ourClassName} + {...otherProps} + /> ); } ScrollIndicatedDialogContent.propTypes = { classes: PropTypes.shape({ shadowScrollDialog: PropTypes.string, - }).isRequired, - + }), className: PropTypes.string, }; ScrollIndicatedDialogContent.defaultProps = { + classes: {}, className: '', }; diff --git a/src/components/ScrollTo.js b/src/components/ScrollTo.js index 7d7f8a82cadfc2af5d31aedc93de2ba5a00d3b22..4e11892d7267729ba6619f090c407910785d3612 100644 --- a/src/components/ScrollTo.js +++ b/src/components/ScrollTo.js @@ -1,5 +1,6 @@ -import { createRef, Component } from 'react'; +import { cloneElement, createRef, Component } from 'react'; import PropTypes from 'prop-types'; +import isEmpty from 'lodash/isEmpty'; /** * ScrollTo ~ @@ -36,9 +37,9 @@ export class ScrollTo extends Component { containerBoundingRect() { const { containerRef } = this.props; - if (!containerRef || !containerRef.current || !containerRef.current.domEl) return {}; + if (!containerRef || !containerRef.current) return {}; - return containerRef.current.domEl.getBoundingClientRect(); + return containerRef.current.getBoundingClientRect(); } /** @@ -59,14 +60,14 @@ export class ScrollTo extends Component { } /** - * The container provided in the containersRef dome structure in which scrolling + * The container provided in the containersRef dom structure in which scrolling * should happen. */ - scrollabelContainer() { + scrollableContainer() { const { containerRef } = this.props; - if (!containerRef || !containerRef.current || !containerRef.current.domEl) return null; - return containerRef.current.domEl.getElementsByClassName('mirador-scrollto-scrollable')[0]; + if (!containerRef || !containerRef.current) return null; + return containerRef.current.getElementsByClassName('mirador-scrollto-scrollable')[0]; } /** @@ -93,25 +94,23 @@ export class ScrollTo extends Component { if (!scrollTo) return; if (!this.elementToScrollTo()) return; if (this.elementIsVisible()) return; - if (!this.scrollabelContainer()) return; + if (!this.scrollableContainer()) return; const scrollBy = this.elementToScrollTo().offsetTop - (this.containerBoundingRect().height / 2) + offsetTop; - this.scrollabelContainer().scrollTo(0, scrollBy); + this.scrollableContainer().scrollTo(0, scrollBy); } /** * Returns the rendered component */ render() { - const { children, scrollTo } = this.props; + const { + children, containerRef, offsetTop, scrollTo, nodeId, ...otherProps + } = this.props; - if (!scrollTo) return children; + if (!scrollTo && isEmpty(otherProps)) return children; - return ( - <div ref={this.scrollToRef}> - {children} - </div> - ); + return cloneElement(children, { ref: this.scrollToRef, ...otherProps }); } } @@ -121,6 +120,7 @@ ScrollTo.propTypes = { PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) }), ]).isRequired, + nodeId: PropTypes.string.isRequired, offsetTop: PropTypes.number, scrollTo: PropTypes.bool.isRequired, }; diff --git a/src/components/SearchHit.js b/src/components/SearchHit.js index 7feac2d9741108b5887cea314b1d62dd8e23e9dd..41a8be606d62b03431e3b9c2a283a8591a8a9b1c 100644 --- a/src/components/SearchHit.js +++ b/src/components/SearchHit.js @@ -1,15 +1,49 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import clsx from 'clsx'; -import Button from '@material-ui/core/Button'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import Typography from '@material-ui/core/Typography'; -import Chip from '@material-ui/core/Chip'; +import Button from '@mui/material/Button'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import Typography from '@mui/material/Typography'; +import Chip from '@mui/material/Chip'; +import { styled } from '@mui/material/styles'; import SanitizedHtml from '../containers/SanitizedHtml'; import TruncatedHit from '../lib/TruncatedHit'; import { ScrollTo } from './ScrollTo'; +const Root = styled(ListItem, { name: 'SearchHit', slot: 'root' })(({ ownerState, theme }) => ({ + '&.Mui-focused': { + '&:hover': { + ...(ownerState.windowSelected && { + backgroundColor: 'inherit', + }), + }, + ...(ownerState.windowSelected && { + backgroundColor: 'inherit', + }), + }, + paddingRight: theme.spacing(1), +})); + +const CanvasLabel = styled('h4', { name: 'SearchHit', slot: 'canvasLabel' })(({ theme }) => ({ + display: 'inline', + marginBottom: theme.spacing(1.5), +})); + +const Counter = styled(Chip, { name: 'SearchHit', slot: 'counter' })(({ ownerState, theme }) => ({ + // eslint-disable-next-line no-nested-ternary + backgroundColor: theme.palette.hitCounter.default, + ...(ownerState.windowSelected && { + backgroundColor: theme.palette.highlights.primary, + }), + ...(ownerState.adjacent && !ownerState.windowSelected && { + backgroundColor: theme.palette.highlights.secondary, + }), + height: 30, + marginRight: theme.spacing(1), + typography: 'subtitle2', + verticalAlign: 'inherit', +})); + /** */ export class SearchHit extends Component { /** */ @@ -55,17 +89,20 @@ export class SearchHit extends Component { const { annotation, annotationLabel, announcer, canvasLabel, hit, index, t, total, } = this.props; - if (!hit) return; + if (!hit || !announcer) return; const truncatedHit = new TruncatedHit(hit, annotation); - announcer([ - t('pagination', { current: index + 1, total }), - canvasLabel, - annotationLabel, - truncatedHit.before, - truncatedHit.match, - truncatedHit.after, - ].join(' ')); + announcer( + [ + t('pagination', { current: index + 1, total }), + canvasLabel, + annotationLabel, + truncatedHit.before, + truncatedHit.match, + truncatedHit.after, + ].join(' '), + 'polite', + ); } /** */ @@ -75,7 +112,6 @@ export class SearchHit extends Component { annotation, annotationLabel, canvasLabel, - classes, companionWindowId, containerRef, hit, @@ -90,8 +126,27 @@ export class SearchHit extends Component { if (focused && !selected) return null; const truncatedHit = focused ? hit : hit && new TruncatedHit(hit, annotation); - const truncated = hit && truncatedHit.before !== hit.before && truncatedHit.after !== hit.after; + const truncated = hit && (truncatedHit.before !== hit.before || truncatedHit.after !== hit.after); const canvasLabelHtmlId = `${companionWindowId}-${index}`; + const ownerState = { + adjacent, focused, selected, windowSelected, + }; + + const header = ( + <> + <Counter + component="span" + ownerState={ownerState} + label={index + 1} + /> + <CanvasLabel id={canvasLabelHtmlId}> + {canvasLabel} + {annotationLabel && ( + <Typography component="span" sx={{ display: 'block', marginTop: 1 }}>{annotationLabel}</Typography> + )} + </CanvasLabel> + </> + ); return ( <ScrollTo @@ -99,51 +154,56 @@ export class SearchHit extends Component { offsetTop={96} // offset for the height of the form above scrollTo={windowSelected && !focused} > - <ListItem - className={clsx( - classes.listItem, - { - [classes.adjacent]: adjacent, - [classes.selected]: selected, - [classes.focused]: focused, - [classes.windowSelected]: windowSelected, - }, - )} + <Root + ownerState={ownerState} + className={windowSelected ? 'windowSelected' : ''} + divider button={!selected} component="li" onClick={this.handleClick} selected={selected} > - <ListItemText primaryTypographyProps={{ variant: 'body1' }}> - <Typography variant="subtitle2" className={classes.subtitle}> - <Chip component="span" label={index + 1} className={classes.hitCounter} /> - <span id={canvasLabelHtmlId}> - {canvasLabel} - </span> - </Typography> - {annotationLabel && ( - <Typography variant="subtitle2">{annotationLabel}</Typography> - )} - {hit && ( + <ListItemText + primary={header} + primaryTypographyProps={{ component: 'div', sx: { marginBottom: 1 }, variant: 'subtitle2' }} + secondaryTypographyProps={{ variant: 'body1' }} + secondary={( <> - <SanitizedHtml ruleSet="iiif" htmlString={truncatedHit.before} /> - {' '} - <strong> - <SanitizedHtml ruleSet="iiif" htmlString={truncatedHit.match} /> - </strong> - {' '} - <SanitizedHtml ruleSet="iiif" htmlString={truncatedHit.after} /> - {' '} - { truncated && !focused && ( - <Button className={classes.inlineButton} onClick={showDetails} color="secondary" size="small" aria-describedby={canvasLabelHtmlId}> - {t('more')} - </Button> + {hit && ( + <> + <SanitizedHtml ruleSet="iiif" htmlString={truncatedHit.before} /> + {' '} + <strong> + <SanitizedHtml ruleSet="iiif" htmlString={truncatedHit.match} /> + </strong> + {' '} + <SanitizedHtml ruleSet="iiif" htmlString={truncatedHit.after} /> + {' '} + {truncated && !focused && ( + <Button + sx={{ + '& span': { + lineHeight: '1.5em', + }, + margin: 0, + padding: 0, + textTransform: 'none', + }} + onClick={showDetails} + color="secondary" + size="small" + aria-describedby={canvasLabelHtmlId} + > + {t('more')} + </Button> + )} + </> )} + {!hit && annotation && <SanitizedHtml ruleSet="iiif" htmlString={annotation.chars} />} </> )} - {!hit && annotation && <SanitizedHtml ruleSet="iiif" htmlString={annotation.chars} />} - </ListItemText> - </ListItem> + /> + </Root> </ScrollTo> ); } @@ -157,9 +217,8 @@ SearchHit.propTypes = { }), annotationId: PropTypes.string, annotationLabel: PropTypes.string, - announcer: PropTypes.func.isRequired, + announcer: PropTypes.func, canvasLabel: PropTypes.string, - classes: PropTypes.objectOf(PropTypes.string), companionWindowId: PropTypes.string, containerRef: PropTypes.oneOfType([ PropTypes.func, @@ -186,8 +245,8 @@ SearchHit.defaultProps = { annotation: undefined, annotationId: undefined, annotationLabel: undefined, + announcer: undefined, canvasLabel: undefined, - classes: {}, companionWindowId: undefined, containerRef: undefined, focused: false, diff --git a/src/components/SearchPanel.js b/src/components/SearchPanel.js index 375e39951f3a43d210b8608e0d003c7689190813..42550dddf38e39fe4681302927f4ad07b09a379b 100644 --- a/src/components/SearchPanel.js +++ b/src/components/SearchPanel.js @@ -1,8 +1,8 @@ import { createRef, Component } from 'react'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import Chip from '@material-ui/core/Chip'; -import Typography from '@material-ui/core/Typography'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Typography from '@mui/material/Typography'; import CompanionWindow from '../containers/CompanionWindow'; import SearchPanelControls from '../containers/SearchPanelControls'; import SearchResults from '../containers/SearchResults'; @@ -19,7 +19,6 @@ export class SearchPanel extends Component { /** */ render() { const { - classes, fetchSearch, windowId, id, @@ -39,12 +38,14 @@ export class SearchPanel extends Component { { query && query !== '' && ( <Chip - className={classes.clearChip} + role="button" + sx={{ marginLeft: 1 }} color="secondary" label={t('clearSearch')} onClick={removeSearch} onDelete={removeSearch} size="small" + tabIndex={0} variant="outlined" /> ) @@ -63,8 +64,12 @@ export class SearchPanel extends Component { /> { fetchSearch && suggestedSearches && query === '' && suggestedSearches.map(search => ( - <Typography component="p" key={search} variant="body1"> - <Button className={classes.inlineButton} color="secondary" onClick={() => fetchSearch(`${searchService.id}?q=${search}`, search)}> + <Typography component="p" key={search} variant="body1" sx={{ margin: 2 }}> + <Button + variant="inlineText" + color="secondary" + onClick={() => fetchSearch(`${searchService.id}?q=${search}`, search)} + > {t('suggestSearch', { query: search })} </Button> </Typography> @@ -76,10 +81,6 @@ export class SearchPanel extends Component { } SearchPanel.propTypes = { - classes: PropTypes.shape({ - clearChip: PropTypes.string, - inlineButton: PropTypes.string, - }), fetchSearch: PropTypes.func, id: PropTypes.string.isRequired, query: PropTypes.string, @@ -93,7 +94,6 @@ SearchPanel.propTypes = { }; SearchPanel.defaultProps = { - classes: {}, fetchSearch: undefined, query: '', suggestedSearches: [], diff --git a/src/components/SearchPanelControls.js b/src/components/SearchPanelControls.js index af2e53f3f7c889c5959bd4a552e41a0224a22662..0ebdc7185a243bf533c1263525b3e5dd39996f07 100644 --- a/src/components/SearchPanelControls.js +++ b/src/components/SearchPanelControls.js @@ -1,15 +1,23 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; import deburr from 'lodash/deburr'; import debounce from 'lodash/debounce'; import isObject from 'lodash/isObject'; -import Autocomplete from '@material-ui/lab/Autocomplete'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import TextField from '@material-ui/core/TextField'; -import SearchIcon from '@material-ui/icons/SearchSharp'; +import Autocomplete from '@mui/material/Autocomplete'; +import CircularProgress from '@mui/material/CircularProgress'; +import TextField from '@mui/material/TextField'; +import InputAdornment from '@mui/material/InputAdornment'; +import SearchIcon from '@mui/icons-material/SearchSharp'; import MiradorMenuButton from '../containers/MiradorMenuButton'; import SearchPanelNavigation from '../containers/SearchPanelNavigation'; +const StyledForm = styled('form', { name: 'SearchPanelControls', slot: 'form' })(({ theme }) => ({ + paddingBottom: theme.spacing(1), + paddingRight: theme.spacing(1.5), + width: '100%', +})); + /** Sometimes an autocomplete match can be a simple string, other times an object with a `match` property, this function abstracts that away */ const getMatch = (option) => (isObject(option) ? option.match : option); @@ -43,6 +51,13 @@ export class SearchPanelControls extends Component { } } + /** + * Cancel the debounce function when the component unmounts + */ + componentWillUnmount() { + this.fetchAutocomplete.cancel(); + } + /** */ handleChange(event, value, reason) { // For some reason the value gets reset to an empty value from the @@ -79,7 +94,7 @@ export class SearchPanelControls extends Component { if (!autocompleteService) return; if (!value) return; - fetch(`${autocompleteService.id}?q=${value}`) + fetch(`${autocompleteService.id}?${new URLSearchParams({ q: value })}`) .then(response => response.json()) .then(this.receiveAutocomplete); } @@ -97,7 +112,7 @@ export class SearchPanelControls extends Component { const { search } = this.state; event && event.preventDefault(); if (!search) return; - fetchSearch(windowId, companionWindowId, `${searchService.id}?q=${search}`, search); + fetchSearch(windowId, companionWindowId, `${searchService.id}?${new URLSearchParams({ q: search })}`, search); } /** */ @@ -110,48 +125,62 @@ export class SearchPanelControls extends Component { /** */ render() { const { - classes, companionWindowId, searchIsFetching, t, windowId, + companionWindowId, searchIsFetching, t, windowId, } = this.props; const { search, suggestions } = this.state; const id = `search-${companionWindowId}`; return ( <> - <form onSubmit={this.submitSearch} className={classes.form}> + <StyledForm + aria-label={t('searchTitle')} + onSubmit={this.submitSearch} + > <Autocomplete id={id} inputValue={search} options={suggestions} getOptionLabel={getMatch} - getOptionSelected={(option, value) => ( + isOptionEqualToValue={(option, value) => ( deburr(getMatch(option).trim()).toLowerCase() - === deburr(getMatch(value).trim()).toLowerCase() + === deburr(getMatch(value).trim()).toLowerCase() )} noOptionsText="" onChange={this.selectItem} onInputChange={this.handleChange} freeSolo + disableClearable renderInput={params => ( <TextField {...params} label={t('searchInputLabel')} + variant="standard" InputProps={{ ...params.InputProps, endAdornment: ( - <div className={classes.endAdornment}> + <InputAdornment sx={{ position: 'relative' }} position="end"> <MiradorMenuButton aria-label={t('searchSubmitAria')} type="submit"> <SearchIcon /> </MiradorMenuButton> {Boolean(searchIsFetching) && ( - <CircularProgress className={classes.searchProgress} size={50} /> + <CircularProgress + sx={{ + left: '50%', + marginLeft: '-25px', + marginTop: '-25px', + position: 'absolute', + top: '50%', + }} + size={50} + /> )} - </div> + </InputAdornment> ), }} /> )} /> - </form> + </StyledForm> <SearchPanelNavigation windowId={windowId} companionWindowId={companionWindowId} /> </> ); @@ -162,7 +191,6 @@ SearchPanelControls.propTypes = { autocompleteService: PropTypes.shape({ id: PropTypes.string, }), - classes: PropTypes.objectOf(PropTypes.string), companionWindowId: PropTypes.string.isRequired, fetchSearch: PropTypes.func.isRequired, query: PropTypes.string, @@ -176,7 +204,6 @@ SearchPanelControls.propTypes = { SearchPanelControls.defaultProps = { autocompleteService: undefined, - classes: {}, query: '', t: key => key, }; diff --git a/src/components/SearchPanelNavigation.js b/src/components/SearchPanelNavigation.js index 72eb84e1fe181a6756d8a32644721fe28e4f7b40..3ddf2c49b5dc8631e09c7002aef30ef731c3f9e9 100644 --- a/src/components/SearchPanelNavigation.js +++ b/src/components/SearchPanelNavigation.js @@ -1,8 +1,8 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import ChevronLeftIcon from '@material-ui/icons/ChevronLeftSharp'; -import ChevronRightIcon from '@material-ui/icons/ChevronRightSharp'; -import Typography from '@material-ui/core/Typography'; +import ChevronLeftIcon from '@mui/icons-material/ChevronLeftSharp'; +import ChevronRightIcon from '@mui/icons-material/ChevronRightSharp'; +import Typography from '@mui/material/Typography'; import MiradorMenuButton from '../containers/MiradorMenuButton'; /** @@ -42,7 +42,7 @@ export class SearchPanelNavigation extends Component { */ render() { const { - numTotal, searchHits, selectedContentSearchAnnotation, classes, t, direction, + numTotal, searchHits, selectedContentSearchAnnotation, t, direction, } = this.props; const iconStyle = direction === 'rtl' ? { transform: 'rotate(180deg)' } : {}; @@ -57,7 +57,7 @@ export class SearchPanelNavigation extends Component { if (searchHits.length === 0) return null; return ( - <Typography variant="body2" align="center" classes={classes}> + <Typography variant="body2" align="center"> <MiradorMenuButton aria-label={t('searchPreviousResult')} disabled={!this.hasPreviousResult(currentHitIndex)} @@ -80,7 +80,6 @@ export class SearchPanelNavigation extends Component { } } SearchPanelNavigation.propTypes = { - classes: PropTypes.objectOf(PropTypes.string), direction: PropTypes.string.isRequired, numTotal: PropTypes.number, searchHits: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types @@ -93,7 +92,6 @@ SearchPanelNavigation.propTypes = { windowId: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types }; SearchPanelNavigation.defaultProps = { - classes: {}, numTotal: undefined, searchHits: [], t: key => key, diff --git a/src/components/SearchResults.js b/src/components/SearchResults.js index 912f9084746e04e8f2e894ec4ba314ff0007eca5..15263db3badcdef2e994cbd17f78073eb9c2c673 100644 --- a/src/components/SearchResults.js +++ b/src/components/SearchResults.js @@ -1,10 +1,10 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import List from '@material-ui/core/List'; -import Typography from '@material-ui/core/Typography'; -import BackIcon from '@material-ui/icons/ArrowBackSharp'; -import { LiveMessenger } from 'react-aria-live'; +import Button from '@mui/material/Button'; +import List from '@mui/material/List'; +import Typography from '@mui/material/Typography'; +import BackIcon from '@mui/icons-material/ArrowBackSharp'; +import { announce } from '@react-aria/live-announcer'; import SearchHit from '../containers/SearchHit'; import { ScrollTo } from './ScrollTo'; @@ -32,7 +32,7 @@ export class SearchResults extends Component { * Return SearchHits for every hit in the response * Return SearchHits for every annotation in the response if there are no hits */ - renderSearchHitsAndAnnotations(announcer) { + renderSearchHitsAndAnnotations() { const { companionWindowId, containerRef, @@ -47,7 +47,7 @@ export class SearchResults extends Component { if (searchHits.length === 0 && searchAnnotations.length > 0) { return searchAnnotations.map((anno, index) => ( <SearchHit - announcer={announcer} + announcer={announce} annotationId={anno.id} companionWindowId={companionWindowId} containerRef={containerRef} @@ -63,7 +63,7 @@ export class SearchResults extends Component { return searchHits.map((hit, index) => ( <SearchHit - announcer={announcer} + announcer={announce} containerRef={containerRef} companionWindowId={companionWindowId} key={hit.annotations[0]} @@ -80,7 +80,6 @@ export class SearchResults extends Component { /** */ render() { const { - classes, companionWindowId, containerRef, isFetching, @@ -106,25 +105,27 @@ export class SearchResults extends Component { <> { focused && ( <ScrollTo containerRef={containerRef} offsetTop={96} scrollTo> - <Button onClick={this.toggleFocus} className={classes.navigation} size="small"> + <Button onClick={this.toggleFocus} sx={{ textTransform: 'none' }} size="small"> <BackIcon /> {t('backToResults')} </Button> </ScrollTo> )} {noResultsState && ( - <Typography className={classes.noResults}> + <Typography sx={{ + padding: 2, + typography: 'h6', + }} + > {t('searchNoResults')} </Typography> )} <List disablePadding> - <LiveMessenger> - {({ announcePolite }) => this.renderSearchHitsAndAnnotations(announcePolite) } - </LiveMessenger> + { this.renderSearchHitsAndAnnotations() } </List> { nextSearch && ( <Button - className={classes.moreButton} + sx={{ width: '100%' }} color="secondary" onClick={() => fetchSearch(windowId, companionWindowId, nextSearch, query)} > @@ -139,7 +140,6 @@ export class SearchResults extends Component { } SearchResults.propTypes = { - classes: PropTypes.objectOf(PropTypes.string), companionWindowId: PropTypes.string.isRequired, containerRef: PropTypes.oneOfType([ PropTypes.func, @@ -157,7 +157,6 @@ SearchResults.propTypes = { }; SearchResults.defaultProps = { - classes: {}, containerRef: undefined, isFetching: false, nextSearch: undefined, diff --git a/src/components/SelectCollection.js b/src/components/SelectCollection.js index c3d275a17933a97e29f70865052c5491908dd804..c51af8f531c25e41baf455a1eacb260a42eae8c2 100644 --- a/src/components/SelectCollection.js +++ b/src/components/SelectCollection.js @@ -1,9 +1,9 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; -import ListSharpIcon from '@material-ui/icons/ListSharp'; +import Button from '@mui/material/Button'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; +import ListSharpIcon from '@mui/icons-material/ListSharp'; /** * @@ -38,6 +38,7 @@ export class SelectCollection extends Component { </em> </Typography> <Button + aria-label="show collection" color="primary" variant="contained" onClick={this.openCollectionDialog} diff --git a/src/components/SidebarIndexItem.js b/src/components/SidebarIndexItem.js index d6b1a96668006e072062bd0cd0b1b0d0d1f9dc4e..11bb4af9c08171d10ee6a9a8be4c059a3a7711b1 100644 --- a/src/components/SidebarIndexItem.js +++ b/src/components/SidebarIndexItem.js @@ -1,19 +1,17 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Typography from '@material-ui/core/Typography'; -import classNames from 'classnames'; +import Typography from '@mui/material/Typography'; /** */ export class SidebarIndexItem extends Component { /** */ render() { const { - classes, label, + label, } = this.props; return ( <Typography - className={classNames(classes.label)} variant="body1" > {label} @@ -23,6 +21,5 @@ export class SidebarIndexItem extends Component { } SidebarIndexItem.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, label: PropTypes.string.isRequired, }; diff --git a/src/components/SidebarIndexList.js b/src/components/SidebarIndexList.js index 104019beddce9ee0cf6397b6f748d5e47bac3805..d19394ef97ec83f0718e161c4171f37446829a0d 100644 --- a/src/components/SidebarIndexList.js +++ b/src/components/SidebarIndexList.js @@ -1,12 +1,21 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import MenuList from '@material-ui/core/MenuList'; -import MenuItem from '@material-ui/core/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import MenuItem from '@mui/material/MenuItem'; +import { styled } from '@mui/material/styles'; import { ScrollTo } from './ScrollTo'; import MiradorCanvas from '../lib/MiradorCanvas'; import SidebarIndexItem from '../containers/SidebarIndexItem'; import SidebarIndexThumbnail from '../containers/SidebarIndexThumbnail'; +const StyledItem = styled(MenuItem, { name: 'SidebarIndexList', slot: 'item' })(({ theme }) => ({ + alignItems: 'flex-start', + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(1), + position: 'initial', + whiteSpace: 'normal', +})); + /** */ export class SidebarIndexList extends Component { /** @private */ @@ -23,7 +32,6 @@ export class SidebarIndexList extends Component { render() { const { canvases, - classes, containerRef, selectedCanvasIds, setCanvas, @@ -53,19 +61,17 @@ export class SidebarIndexList extends Component { containerRef={containerRef} key={`${canvas.id}-${variant}`} offsetTop={96} // offset for the height of the form above + selected={selectedCanvasIds.includes(canvas.id)} scrollTo={selectedCanvasIds.includes(canvas.id)} > - <MenuItem + <StyledItem key={canvas.id} - className={classes.listItem} - alignItems="flex-start" + divider onClick={onClick} - button component="li" - selected={selectedCanvasIds.includes(canvas.id)} > <Item label={canvas.label} canvas={canvases[canvasIndex]} /> - </MenuItem> + </StyledItem> </ScrollTo> ); }) @@ -77,7 +83,6 @@ export class SidebarIndexList extends Component { SidebarIndexList.propTypes = { canvases: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types - classes: PropTypes.objectOf(PropTypes.string).isRequired, containerRef: PropTypes.oneOf([PropTypes.func, PropTypes.object]).isRequired, selectedCanvasIds: PropTypes.arrayOf(PropTypes.string), setCanvas: PropTypes.func.isRequired, diff --git a/src/components/SidebarIndexTableOfContents.js b/src/components/SidebarIndexTableOfContents.js index bfcf61bff1c41202fa20eef786f667d5da60c1ff..296555e8c4ef73db40f56f37dea7211c62fa5492 100644 --- a/src/components/SidebarIndexTableOfContents.js +++ b/src/components/SidebarIndexTableOfContents.js @@ -1,12 +1,15 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import TreeView from '@material-ui/lab/TreeView'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import ChevronRightIcon from '@material-ui/icons/ChevronRight'; -import TreeItem from '@material-ui/lab/TreeItem'; -import clsx from 'clsx'; +import { alpha, styled } from '@mui/material/styles'; +import { TreeView } from '@mui/x-tree-view/TreeView'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import { TreeItem } from '@mui/x-tree-view/TreeItem'; import { ScrollTo } from './ScrollTo'; +const StyledVisibleNode = styled('div')(() => ({ + +})); /** */ function getStartCanvasId(node) { const jsonld = node.data.__jsonld; // eslint-disable-line no-underscore-dangle @@ -24,29 +27,70 @@ function getStartCanvasId(node) { return node.data.getCanvasIds()[0]; } +/** Traverse through the manifesto tree to find a node with a given id */ +function deepFind(treeNode, id) { + if (treeNode.id === id) { + return treeNode; + } + + let result = null; + + if (treeNode.nodes) { + for (let i = 0; result == null && i < treeNode.nodes.length; i += 1) { + result = deepFind(treeNode.nodes[i], id); + } + } + + return result; +} + +/** Wrap <ScrollTo> to remove the nodeId prop required for MUI's TreeView */ +const ScrollToForTreeItem = ({ children, nodeId, ...props }) => ( + <ScrollTo + {...props} + > + { children } + </ScrollTo> +); + +ScrollToForTreeItem.propTypes = { + children: PropTypes.node.isRequired, + nodeId: PropTypes.string.isRequired, +}; + /** */ export class SidebarIndexTableOfContents extends Component { /** */ - handleKeyPressed(event, node) { - const { expandedNodeIds, toggleNode } = this.props; - if (event.key === 'Enter' - || event.key === ' ' - || event.key === 'Spacebar') { - this.selectTreeItem(node); - } - if ((event.key === 'ArrowLeft' && expandedNodeIds.indexOf(node.id) !== -1) - || (event.key === 'ArrowRight' && expandedNodeIds.indexOf(node.id) === -1 && node.nodes.length > 0)) { - toggleNode(node.id); - } + constructor(props) { + super(props); + this.handleNodeSelect = this.handleNodeSelect.bind(this); + this.handleNodeToggle = this.handleNodeToggle.bind(this); } /** */ - selectTreeItem(node) { - const { setCanvas, toggleNode, windowId } = this.props; - if (node.nodes.length > 0) { - toggleNode(node.id); + handleNodeSelect(event, nodeId) { + const { toggleNode } = this.props; + + if (event.key === ' ' || event.key === 'Spacebar') { + toggleNode(nodeId); } + this.selectTreeItem(nodeId); + } + + /** */ + handleNodeToggle(_event, nodeIds) { + const { expandNodes } = this.props; + + expandNodes(nodeIds); + } + + /** */ + selectTreeItem(nodeId) { + const { setCanvas, treeStructure, windowId } = this.props; + + const node = deepFind(treeStructure, nodeId); + // Do not select if there are no canvases listed or it has children if (!node.data.getCanvasIds() || node.data.getCanvasIds().length === 0 @@ -58,85 +102,96 @@ export class SidebarIndexTableOfContents extends Component { setCanvas(windowId, canvasId); } - /** */ - buildTreeItems(nodes, visibleNodeIds, containerRef, nodeIdToScrollTo) { - const { classes } = this.props; - if (!nodes) { - return null; - } - return ( - nodes.map(node => ( - <ScrollTo - containerRef={containerRef} - key={`${node.id}-scroll`} - offsetTop={96} // offset for the height of the form above - scrollTo={nodeIdToScrollTo === node.id} - > - <TreeItem - key={node.id} - nodeId={node.id} - classes={{ - content: classes.content, - group: classes.group, - label: classes.label, - root: classes.treeItemRoot, - selected: classes.selected, - }} - label={( - <div - className={clsx({ - [classes.visibleNode]: visibleNodeIds.indexOf(node.id) !== -1, - })} - > - {node.label} - </div> - )} - onClick={() => this.selectTreeItem(node)} - onKeyDown={e => this.handleKeyPressed(e, node)} - > - {node.nodes && node.nodes.length > 0 ? this.buildTreeItems( - node.nodes, - visibleNodeIds, - containerRef, - nodeIdToScrollTo, - ) : null} - </TreeItem> - </ScrollTo> - )) - ); - } - /** */ render() { const { - classes, treeStructure, visibleNodeIds, expandedNodeIds, containerRef, nodeIdToScrollTo, + treeStructure, visibleNodeIds, expandedNodeIds, containerRef, nodeIdToScrollTo, } = this.props; if (!treeStructure) { return null; } + /** Render the tree structure recursively */ + const renderTree = (node) => ( + <ScrollToForTreeItem + containerRef={containerRef} + key={node.id} + nodeId={node.id} + offsetTop={96} // offset for the height of the form above + scrollTo={nodeIdToScrollTo === node.id} + > + <TreeItem + nodeId={node.id} + sx={{ + '& .MuiTreeItem-content': { + alignItems: 'flex-start', + borderLeft: '1px solid transparent', + padding: '8px 16px 8px 0', + width: 'auto', + }, + '& .MuiTreeItem-group': { + borderLeft: '1px solid', + borderLeftColor: 'grey.300', + }, + '& .MuiTreeItem-iconContainer': { + paddingBlockStart: 0.5, + }, + '& .MuiTreeItem-label': { + paddingLeft: 0, + }, + '& .MuiTreeItem-root': { + '&:focus > .MuiTreeItem-content': { + backgroundColor: 'action.selected', + }, + '&:hover > .MuiTreeItem-content': { + backgroundColor: 'action.hover', + }, + '&:hover > .MuiTreeItem-content .MuiTreeItem-label, &:focus > .MuiTreeItem-content .MuiTreeItem-label, &.MuiTreeItem-selected > .MuiTreeItem-content .MuiTreeItem-label, &.MuiTreeItem-selected > .MuiTreeItem-content .MuiTreeItem-label:hover, &.MuiTreeItem-selected:focus > .MuiTreeItem-content .MuiTreeItem-label': { + backgroundColor: 'transparent', + }, + }, + }} + label={( + <StyledVisibleNode + sx={theme => ({ + backgroundColor: visibleNodeIds.indexOf(node.id) !== -1 + && alpha(theme.palette.highlights?.primary || theme.palette.action.selected, 0.35), + display: visibleNodeIds.indexOf(node.id) !== -1 && 'inline', + })} + > + {node.label} + </StyledVisibleNode> + )} + > + {Array.isArray(node.nodes) ? node.nodes.map((n) => renderTree(n)) : null} + </TreeItem> + </ScrollToForTreeItem> + ); + return ( <TreeView - className={classes.root} + sx={{ flexGrow: 1 }} defaultCollapseIcon={<ExpandMoreIcon color="action" />} defaultExpandIcon={<ChevronRightIcon color="action" />} defaultEndIcon={null} + onNodeSelect={this.handleNodeSelect} + onNodeToggle={this.handleNodeToggle} expanded={expandedNodeIds} > - {this.buildTreeItems(treeStructure.nodes, visibleNodeIds, containerRef, nodeIdToScrollTo)} + { Array.isArray(treeStructure.nodes) ? treeStructure.nodes.map(n => renderTree(n)) : null } </TreeView> ); } } SidebarIndexTableOfContents.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, containerRef: PropTypes.oneOfType([ PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) }), ]).isRequired, expandedNodeIds: PropTypes.arrayOf(PropTypes.string).isRequired, + expandNodes: PropTypes.func.isRequired, nodeIdToScrollTo: PropTypes.func.isRequired, setCanvas: PropTypes.func.isRequired, toggleNode: PropTypes.func.isRequired, diff --git a/src/components/SidebarIndexThumbnail.js b/src/components/SidebarIndexThumbnail.js index 7abd1973d6051d0e71e4c18bbaf40bd3a4031669..05126d8de02dc6d6d184bea3d94bb838607e9335 100644 --- a/src/components/SidebarIndexThumbnail.js +++ b/src/components/SidebarIndexThumbnail.js @@ -1,7 +1,6 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Typography from '@material-ui/core/Typography'; -import classNames from 'classnames'; +import Typography from '@mui/material/Typography'; import IIIFThumbnail from '../containers/IIIFThumbnail'; /** */ @@ -9,7 +8,7 @@ export class SidebarIndexThumbnail extends Component { /** */ render() { const { - classes, canvas, height, label, width, + canvas, height, label, width, } = this.props; return ( @@ -18,15 +17,11 @@ export class SidebarIndexThumbnail extends Component { <IIIFThumbnail label={label} resource={canvas} - className={classNames(classes.clickable)} maxHeight={height} maxWidth={width} /> </div> - <Typography - className={classNames(classes.label)} - variant="body1" - > + <Typography> {label} </Typography> </> @@ -36,7 +31,6 @@ export class SidebarIndexThumbnail extends Component { SidebarIndexThumbnail.propTypes = { canvas: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types - classes: PropTypes.objectOf(PropTypes.string).isRequired, height: PropTypes.number, label: PropTypes.string.isRequired, width: PropTypes.number, diff --git a/src/components/ThumbnailCanvasGrouping.js b/src/components/ThumbnailCanvasGrouping.js index 6e8902339c361816aad67a35f270418eee0eed4b..f01d399260c4e032e300683e73c133cec53cc130 100644 --- a/src/components/ThumbnailCanvasGrouping.js +++ b/src/components/ThumbnailCanvasGrouping.js @@ -1,9 +1,17 @@ import { PureComponent } from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; import classNames from 'classnames'; import IIIFThumbnail from '../containers/IIIFThumbnail'; import ns from '../config/css-ns'; +const StyledCanvas = styled('div')(({ theme }) => ({ + boxSizing: 'border-box', + color: theme.palette.common.white, + cursor: 'pointer', + display: 'inline-block', + whiteSpace: 'nowrap', +})); /** */ export class ThumbnailCanvasGrouping extends PureComponent { /** */ @@ -31,7 +39,7 @@ export class ThumbnailCanvasGrouping extends PureComponent { /** */ render() { const { - index, style, data, classes, currentCanvasId, + index, style, data, currentCanvasId, } = this.props; const { canvasGroupings, position, height, @@ -52,24 +60,27 @@ export class ThumbnailCanvasGrouping extends PureComponent { role="gridcell" aria-colindex={index + 1} > - <div + <StyledCanvas role="button" data-canvas-id={currentGroupings[0].id} data-canvas-index={currentGroupings[0].index} onKeyUp={this.setCanvas} onClick={this.setCanvas} tabIndex={-1} - style={{ + sx={theme => ({ + '&:hover': { + outline: `9px solid ${theme.palette.action.hover}`, + outlineOffset: '-2px', + }, height: (position === 'far-right') ? 'auto' : `${height - SPACING}px`, + outline: currentGroupings.map(canvas => canvas.id).includes(currentCanvasId) ? `2px solid ${theme.palette.primary.main}` : 0, + ...(currentGroupings.map(canvas => canvas.id).includes(currentCanvasId) && { + outlineOffset: '3px', + }), width: (position === 'far-bottom') ? 'auto' : `${style.width}px`, - }} + })} className={classNames( ns(['thumbnail-nav-canvas', `thumbnail-nav-canvas-${index}`, this.currentCanvasClass(currentGroupings.map(canvas => canvas.index))]), - classes.canvas, - { - [classes.currentCanvas]: currentGroupings - .map(canvas => canvas.id).includes(currentCanvasId), - }, )} > {currentGroupings.map((canvas, i) => ( @@ -81,14 +92,13 @@ export class ThumbnailCanvasGrouping extends PureComponent { variant="inside" /> ))} - </div> + </StyledCanvas> </div> ); } } ThumbnailCanvasGrouping.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, currentCanvasId: PropTypes.string.isRequired, data: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types index: PropTypes.number.isRequired, diff --git a/src/components/ThumbnailNavigation.js b/src/components/ThumbnailNavigation.js index 4dda633c9471d4e7ad88621b781603e95bb350f5..1a4ce20cf69e8e7c9daf22151cc1c8aa4cadab01 100644 --- a/src/components/ThumbnailNavigation.js +++ b/src/components/ThumbnailNavigation.js @@ -1,6 +1,6 @@ import { createRef, Component } from 'react'; import PropTypes from 'prop-types'; -import Paper from '@material-ui/core/Paper'; +import Paper from '@mui/material/Paper'; import AutoSizer from 'react-virtualized-auto-sizer'; import { VariableSizeList as List } from 'react-window'; import classNames from 'classnames'; @@ -20,7 +20,7 @@ export class ThumbnailNavigation extends Component { this.spacing = 8; // 2 * (2px margin + 2px border + 2px padding + 2px padding) this.calculateScaledSize = this.calculateScaledSize.bind(this); this.itemCount = this.itemCount.bind(this); - this.handleKeyUp = this.handleKeyUp.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); this.nextCanvas = this.nextCanvas.bind(this); this.previousCanvas = this.previousCanvas.bind(this); this.gridRef = createRef(); @@ -43,7 +43,7 @@ export class ThumbnailNavigation extends Component { } /** */ - handleKeyUp(e) { + handleKeyDown(e) { const { position } = this.props; let nextKey = 'ArrowRight'; let previousKey = 'ArrowLeft'; @@ -176,7 +176,6 @@ export class ThumbnailNavigation extends Component { const { t, canvasGroupings, - classes, position, thumbnailNavigation, viewingDirection, @@ -196,14 +195,19 @@ export class ThumbnailNavigation extends Component { <Paper className={classNames( ns('thumb-navigation'), - classes.thumbNavigation, )} + sx={{ + '&:focus': { + boxShadow: 0, + outline: 0, + }, + }} aria-label={t('thumbnailNavigation')} square elevation={0} style={this.style()} tabIndex={0} - onKeyUp={this.handleKeyUp} + onKeyDown={this.handleKeyDown} role="grid" > <div role="row" style={{ height: '100%', width: '100%' }}> @@ -235,7 +239,6 @@ export class ThumbnailNavigation extends Component { ThumbnailNavigation.propTypes = { canvasGroupings: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types canvasIndex: PropTypes.number.isRequired, - classes: PropTypes.objectOf(PropTypes.string).isRequired, hasNextCanvas: PropTypes.bool, hasPreviousCanvas: PropTypes.bool, position: PropTypes.string.isRequired, diff --git a/src/components/VideoViewer.js b/src/components/VideoViewer.js index df752439da6d547a8c80d7f484849b6e1220fa9a..9b6c21bdfe44256617cc458fc3b1657676c04c6f 100644 --- a/src/components/VideoViewer.js +++ b/src/components/VideoViewer.js @@ -2,6 +2,18 @@ import flatten from 'lodash/flatten'; import flattenDeep from 'lodash/flattenDeep'; import { createRef, Component } from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; + +const StyledContainer = styled('div')(() => ({ + alignItems: 'center', + display: 'flex', + width: '100%', +})); + +const StyledVideo = styled('video')(() => ({ + maxHeight: '100%', + width: '100%', +})); import AnnotationItem from '../lib/AnnotationItem'; import AnnotationsOverlayVideo from '../containers/AnnotationsOverlayVideo'; import WindowCanvasNavigationControlsVideo from '../containers/WindowCanvasNavigationControlsVideo'; @@ -106,7 +118,7 @@ export class VideoViewer extends Component { /** */ render() { const { - annotations, canvas, classes, currentTime, videoOptions, windowId, + captions, annotations, canvas, currentTime, videoOptions, windowId, } = this.props; const videoResources = flatten( @@ -140,20 +152,24 @@ export class VideoViewer extends Component { const videoTargetTemporalfragment = len > 0 ? videoResources[len - 1].temporalfragment : []; return ( - <div className={classes.flexContainer}> - <div className={classes.flexFill}> - { video && ( + <StyledContainer> + <StyledVideo {...videoOptions}> + {videoResources.map(video => ( <> - <video className={classes.video} key={video.id} ref={this.videoRef} {...videoOptions}> + <Fragment key={video.id}> <source src={video.id} type={video.getFormat()} /> - { vttContent.map(vttc => (<track key={vttc.id} src={vttc.id} srcLang={vttc.language} />)) } - </video> + </Fragment> <AnnotationsOverlayVideo windowId={windowId} videoRef={this.videoRef} videoTarget={videoTargetTemporalfragment} key={`${windowId} ${video.id}`} /> </> - )} - <WindowCanvasNavigationControlsVideo windowId={windowId} /> - </div> - </div> + ))} + {captions.map(caption => ( + <Fragment key={caption.id}> + <track src={caption.id} label={caption.getDefaultLabel()} srcLang={caption.getProperty('language')} /> + </Fragment> + ))} + </StyledVideo> + <WindowCanvasNavigationControlsVideo windowId={windowId} /> + </StyledContainer> ); } /* eslint-enable jsx-a11y/media-has-caption */ @@ -170,12 +186,15 @@ VideoViewer.propTypes = { setHasTextTrack: PropTypes.func, setPaused: PropTypes.func, textTrackDisabled: PropTypes.bool, + captions: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types videoOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types + videoResources: PropTypes.arrayOf(PropTypes.object), windowId: PropTypes.string.isRequired, }; VideoViewer.defaultProps = { annotations: [], + captions: [], canvas: {}, currentTime: 0, muted: false, @@ -185,4 +204,5 @@ VideoViewer.defaultProps = { setPaused: () => {}, textTrackDisabled: true, videoOptions: {}, + videoResources: [], }; diff --git a/src/components/ViewerInfo.js b/src/components/ViewerInfo.js index 595ed37d0f7c94bbef408772c3e39c419a14a481..1958bea5e714189630e6ecc586dcaa0f7b5b646b 100644 --- a/src/components/ViewerInfo.js +++ b/src/components/ViewerInfo.js @@ -1,9 +1,19 @@ import { Component } from 'react'; -import Typography from '@material-ui/core/Typography'; +import Typography from '@mui/material/Typography'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; import classNames from 'classnames'; import ns from '../config/css-ns'; +const StyledOsdInfo = styled('div')(() => ({ + overflow: 'hidden', + paddingBottom: 0.5, + textOverflow: 'ellipsis', + unicodeBidi: 'plaintext', + whiteSpace: 'nowrap', + width: '100%', +})); + /** * */ @@ -14,19 +24,18 @@ export class ViewerInfo extends Component { canvasCount, canvasIndex, canvasLabel, - classes, t, } = this.props; return ( - <div className={classNames(ns('osd-info'), classes.osdInfo)}> + <StyledOsdInfo className={classNames(ns('osd-info'))}> <Typography display="inline" variant="caption" className={ns('canvas-count')}> { t('pagination', { current: canvasIndex + 1, total: canvasCount }) } </Typography> <Typography display="inline" variant="caption" className={ns('canvas-label')}> {canvasLabel && ` • ${canvasLabel}`} </Typography> - </div> + </StyledOsdInfo> ); } } @@ -40,6 +49,5 @@ ViewerInfo.propTypes = { canvasCount: PropTypes.number.isRequired, canvasIndex: PropTypes.number.isRequired, canvasLabel: PropTypes.string, - classes: PropTypes.objectOf(PropTypes.string).isRequired, t: PropTypes.func, }; diff --git a/src/components/ViewerNavigation.js b/src/components/ViewerNavigation.js index 3a659397c55fa06765254aaf4d40644248dd35b8..359b8021198cae4310eccb3aa0f5c0b9bb4b7aec 100644 --- a/src/components/ViewerNavigation.js +++ b/src/components/ViewerNavigation.js @@ -1,5 +1,5 @@ import { Component } from 'react'; -import NavigationIcon from '@material-ui/icons/PlayCircleOutlineSharp'; +import NavigationIcon from '@mui/icons-material/PlayCircleOutlineSharp'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import MiradorMenuButton from '../containers/MiradorMenuButton'; @@ -42,7 +42,7 @@ export class ViewerNavigation extends Component { return ( <div - className={classNames(ns('osd-navigation'), classes.osdNavigation)} + className={classNames(ns('osd-navigation'))} dir={htmlDir} > <MiradorMenuButton diff --git a/src/components/Window.js b/src/components/Window.js index 6033c76919ba2e2364984d351fa2267eb4d93447..c2f6b3a5bb0527e3e68e77432bea9290c3c1ce10 100644 --- a/src/components/Window.js +++ b/src/components/Window.js @@ -1,7 +1,7 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import cn from 'classnames'; -import Paper from '@material-ui/core/Paper'; +import { styled } from '@mui/material/styles'; +import Paper from '@mui/material/Paper'; import { MosaicWindowContext } from 'react-mosaic-component/lib/contextTypes'; import ns from '../config/css-ns'; import WindowTopBar from '../containers/WindowTopBar'; @@ -12,6 +12,58 @@ import ErrorContent from '../containers/ErrorContent'; import IIIFAuthentication from '../containers/IIIFAuthentication'; import { PluginHook } from './PluginHook'; +const Root = styled(Paper)(({ ownerState, theme }) => ({ + backgroundColor: theme.palette.shades?.dark, + borderRadius: 0, + display: 'flex', + flexDirection: 'column', + height: '100%', + minHeight: 0, + overflow: 'hidden', + width: '100%', + ...(ownerState?.maximized && { + left: 0, + position: 'absolute', + top: 0, + zIndex: theme.zIndex.modal - 1, + }), +})); + +const StyledMiddle = styled('div')(() => ({ + display: 'flex', + flex: '1', + flexDirection: 'row', + minHeight: 0, +})); + +const StyledMiddleLeft = styled('div')(() => ({ + display: 'flex', + flex: '1', + flexDirection: 'column', + minHeight: 0, +})); + +const StyledPrimaryWindow = styled('div')(() => ({ + display: 'flex', + flex: '1', + height: '300px', + minHeight: 0, + position: 'relative', +})); + +const StyledCompanionAreaBottom = styled('div')(() => ({ + display: 'flex', + flex: '0', + flexBasis: 'auto', + minHeight: 0, +})); + +const StyledCompanionAreaRight = styled('div')(() => ({ + display: 'flex', + flex: '0 1 auto', + minHeight: 0, +})); + /** * Represents a Window in the mirador workspace * @param {object} window @@ -61,8 +113,8 @@ export class Window extends Component { */ render() { const { - focusWindow, label, isFetching, maximized, sideBarOpen, - view, windowId, classes, t, + focusWindow, label, isFetching, sideBarOpen, + view, windowId, t, manifestError, } = this.props; @@ -77,44 +129,39 @@ export class Window extends Component { } return ( - <Paper + <Root onFocus={focusWindow} + ownerState={this.props} component="section" elevation={1} id={windowId} - className={ - cn( - classes.window, - ns('window'), - maximized ? classes.maximized : null, - ) -} + className={ns('window')} aria-label={t('window', { label })} > {this.wrappedTopBar()} { manifestError && <ErrorContent error={{ stack: manifestError }} windowId={windowId} /> } - <div className={classes.middle}> - <div className={classes.middleLeft}> - <div className={classes.primaryWindow}> + <StyledMiddle> + <StyledMiddleLeft> + <StyledPrimaryWindow> <PrimaryWindow view={view} windowId={windowId} isFetching={isFetching} sideBarOpen={sideBarOpen} /> - </div> - <div className={classes.companionAreaBottom}> + </StyledPrimaryWindow> + <StyledCompanionAreaBottom> <CompanionArea windowId={windowId} position="bottom" /> - </div> - </div> - <div className={classes.companionAreaRight}> + </StyledCompanionAreaBottom> + </StyledMiddleLeft> + <StyledCompanionAreaRight> <CompanionArea windowId={windowId} position="right" /> <CompanionArea windowId={windowId} position="far-right" /> - </div> - </div> + </StyledCompanionAreaRight> + </StyledMiddle> <CompanionArea windowId={windowId} position="far-bottom" /> <PluginHook {...this.props} /> - </Paper> + </Root> ); } } @@ -122,7 +169,6 @@ export class Window extends Component { Window.contextType = MosaicWindowContext; Window.propTypes = { - classes: PropTypes.objectOf(PropTypes.string), focusWindow: PropTypes.func, isFetching: PropTypes.bool, label: PropTypes.string, @@ -137,7 +183,6 @@ Window.propTypes = { }; Window.defaultProps = { - classes: {}, focusWindow: () => {}, isFetching: false, label: null, diff --git a/src/components/WindowAuthenticationBar.js b/src/components/WindowAuthenticationBar.js index bbfbd5a5388cc7889297364298b394c23edcdb96..05d2a365f69dd75d1c3bd331d9c8d8a9079e376f 100644 --- a/src/components/WindowAuthenticationBar.js +++ b/src/components/WindowAuthenticationBar.js @@ -1,14 +1,26 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import Paper from '@material-ui/core/Paper'; -import Collapse from '@material-ui/core/Collapse'; -import DialogActions from '@material-ui/core/DialogActions'; -import Typography from '@material-ui/core/Typography'; -import LockIcon from '@material-ui/icons/LockSharp'; +import { alpha, styled } from '@mui/material/styles'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import Collapse from '@mui/material/Collapse'; +import DialogActions from '@mui/material/DialogActions'; +import Typography from '@mui/material/Typography'; +import LockIcon from '@mui/icons-material/LockSharp'; import SanitizedHtml from '../containers/SanitizedHtml'; import { PluginHook } from './PluginHook'; +const StyledTopBar = styled('div')(({ theme }) => ({ + '&:hover': { + backgroundColor: theme.palette.secondary.main, + }, + alignItems: 'center', + display: 'flex', +})); + +const StyledFauxButton = styled('span')(({ theme }) => ({ + marginLeft: theme.spacing(2.5), +})); /** */ export class WindowAuthenticationBar extends Component { /** */ @@ -35,7 +47,7 @@ export class WindowAuthenticationBar extends Component { /** */ render() { const { - classes, confirmButton, continueLabel, + confirmButton, continueLabel, header, description, icon, label, t, ruleSet, hasLogoutService, status, ConfirmProps, } = this.props; @@ -45,47 +57,90 @@ export class WindowAuthenticationBar extends Component { const { open } = this.state; const button = ( - <Button onClick={this.onSubmit} className={classes.buttonInvert} color="secondary" {...ConfirmProps}> + <Button + onClick={this.onSubmit} + color="secondary" + sx={(theme) => ({ + '&:hover': { + backgroundColor: alpha(theme.palette.secondary.contrastText, 1 - theme.palette.action.hoverOpacity), + }, + backgroundColor: theme.palette.secondary.contrastText, + })} + {...ConfirmProps} + > {confirmButton || t('login')} </Button> ); if (!description && !header) { return ( - <Paper square elevation={4} color="secondary" classes={{ root: classes.paper }}> - <div className={classes.topBar}> - { icon || <LockIcon className={classes.icon} /> } - <Typography className={classes.label} component="h3" variant="body1" color="inherit"> + <Paper + square + elevation={4} + color="secondary" + > + <StyledTopBar> + { icon || ( + <LockIcon sx={{ marginInlineEnd: 1.5 }} /> + ) } + <Typography component="h3" variant="body1" color="inherit"> { ruleSet ? <SanitizedHtml htmlString={label} ruleSet={ruleSet} /> : label } </Typography> <PluginHook {...this.props} /> { button } - </div> + </StyledTopBar> </Paper> ); } return ( - <Paper square elevation={4} color="secondary" classes={{ root: classes.paper }}> - <Button fullWidth className={classes.topBar} onClick={() => this.setOpen(true)} component="div" color="inherit"> - { icon || <LockIcon className={classes.icon} /> } - <Typography className={classes.label} component="h3" variant="body1" color="inherit"> + <Paper + square + elevation={4} + color="secondary" + > + <Button + fullWidth + onClick={() => this.setOpen(true)} + component="div" + color="inherit" + sx={(theme) => ({ + '&:hover': { + backgroundColor: theme.palette.secondary.main, + }, + backgroundColor: theme.palette.secondary.main, + borderRadius: 0, + color: theme.palette.secondary.contrastText, + justifyContent: 'start', + textTransform: 'none', + })} + > + { icon || ( + <LockIcon sx={{ marginInlineEnd: 1.5 }} /> + ) } + <Typography sx={{ paddingBlockEnd: 1, paddingBlockStart: 1 }} component="h3" variant="body1" color="inherit"> { ruleSet ? <SanitizedHtml htmlString={label} ruleSet={ruleSet} /> : label } </Typography> <PluginHook {...this.props} /> - <span className={classes.fauxButton}> + <StyledFauxButton> { !open && ( <Typography variant="button" color="inherit"> { continueLabel || t('continue') } </Typography> )} - </span> + </StyledFauxButton> </Button> <Collapse + sx={(theme) => ({ + backgroundColor: theme.palette.secondary.main, + color: theme.palette.secondary.contrastText, + paddingInlineEnd: theme.spacing(1), + paddingInlineStart: theme.spacing(1), + })} in={open} onClose={() => this.setOpen(false)} > - <Typography variant="body1" color="inherit" className={classes.expanded}> + <Typography variant="body1" color="inherit"> { ruleSet ? <SanitizedHtml htmlString={header} ruleSet={ruleSet} /> : header } { header && description ? ': ' : '' } { ruleSet ? <SanitizedHtml htmlString={description} ruleSet={ruleSet} /> : description } @@ -104,7 +159,6 @@ export class WindowAuthenticationBar extends Component { } WindowAuthenticationBar.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, confirmButton: PropTypes.string, ConfirmProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types continueLabel: PropTypes.string, diff --git a/src/components/WindowCanvasNavigationControls.js b/src/components/WindowCanvasNavigationControls.js index eaee4dc152d82477faf268ccb1f636010fda3647..df9e3ae73788c18d6993a394211704d329d8edda 100644 --- a/src/components/WindowCanvasNavigationControls.js +++ b/src/components/WindowCanvasNavigationControls.js @@ -1,14 +1,33 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; +import { alpha, styled } from '@mui/material/styles'; import classNames from 'classnames'; -import Paper from '@material-ui/core/Paper'; -import Typography from '@material-ui/core/Typography'; +import Paper from '@mui/material/Paper'; +import Stack from '@mui/material/Stack'; +import Divider from '@mui/material/Divider'; +import Typography from '@mui/material/Typography'; +import { visuallyHidden } from '@mui/utils'; import ZoomControls from '../containers/ZoomControls'; import ViewerInfo from '../containers/ViewerInfo'; import ViewerNavigation from '../containers/ViewerNavigation'; import ns from '../config/css-ns'; import { PluginHook } from './PluginHook'; +const Root = styled(Paper, { name: 'WindowCanvasNavigationControls', slot: 'root' })(({ theme }) => ({ + alignItems: 'center', + backgroundColor: alpha(theme.palette.background.paper, 0.5), + bottom: 0, + cursor: 'default', + display: 'flex', + flexDirection: 'column', + flexWrap: 'wrap', + justifyContent: 'center', + position: 'absolute', + textAlign: 'center', + width: '100%', + zIndex: 50, +})); + /** * Represents the viewer controls in the mirador workspace. */ @@ -25,41 +44,40 @@ export class WindowCanvasNavigationControls extends Component { /** */ render() { const { - classes, visible, windowId, zoomToWorld, + showZoomControls, visible, windowId, zoomToWorld, } = this.props; - if (!visible) return (<Typography variant="srOnly" component="div"><ViewerInfo windowId={windowId} /></Typography>); + if (!visible) return (<Typography style={visuallyHidden} component="div"><ViewerInfo windowId={windowId} /></Typography>); return ( - <Paper + <Root square className={ classNames( - classes.controls, ns('canvas-nav'), - classes.canvasNav, this.canvasNavControlsAreStacked() ? ns('canvas-nav-stacked') : null, - this.canvasNavControlsAreStacked() ? classes.canvasNavStacked : null, ) -} + } elevation={0} > - <ZoomControls - displayDivider={!this.canvasNavControlsAreStacked()} - windowId={windowId} - zoomToWorld={zoomToWorld} - /> - <ViewerNavigation windowId={windowId} /> + <Stack + direction={this.canvasNavControlsAreStacked() ? 'column' : 'row'} + divider={<Divider orientation={this.canvasNavControlsAreStacked() ? 'horizontal' : 'vertical'} variant="middle" flexItem />} + spacing={0} + > + { showZoomControls && <ZoomControls windowId={windowId} zoomToWorld={zoomToWorld} /> } + <ViewerNavigation windowId={windowId} /> + </Stack> <ViewerInfo windowId={windowId} /> <PluginHook {...this.props} /> - </Paper> + </Root> ); } } WindowCanvasNavigationControls.propTypes = { - classes: PropTypes.objectOf(PropTypes.string), + showZoomControls: PropTypes.bool, size: PropTypes.shape({ width: PropTypes.number }).isRequired, visible: PropTypes.bool, windowId: PropTypes.string.isRequired, @@ -67,6 +85,6 @@ WindowCanvasNavigationControls.propTypes = { }; WindowCanvasNavigationControls.defaultProps = { - classes: {}, + showZoomControls: false, visible: true, }; diff --git a/src/components/WindowList.js b/src/components/WindowList.js index 6b3adfcb9a6d88ee4b4761c4622732b8626c1300..2db96ba42b70f6672147013dfadac62309f3ad2f 100644 --- a/src/components/WindowList.js +++ b/src/components/WindowList.js @@ -1,24 +1,13 @@ import { Component } from 'react'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import ListSubheader from '@material-ui/core/ListSubheader'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemText from '@mui/material/ListItemText'; +import ListSubheader from '@mui/material/ListSubheader'; import PropTypes from 'prop-types'; -import ns from '../config/css-ns'; /** */ export class WindowList extends Component { - /** - * Given the menuElement passed in by the onEntering callback, - * find the 2nd ListItem element (avoiding the header) and focus it - */ - static focus2ndListIitem(menuElement) { - if (!menuElement.querySelectorAll('li') || menuElement.querySelectorAll('li').length < 2) return; - - menuElement.querySelectorAll('li')[1].focus(); // The 2nd LI - } - /** * Get the title for a window from its manifest title * @private @@ -35,7 +24,8 @@ export class WindowList extends Component { */ render() { const { - containerId, handleClose, anchorEl, windowIds, focusWindow, t, + container, handleClose, windowIds, focusWindow, focusedWindowId, t, + ...menuProps } = this.props; return ( @@ -49,14 +39,9 @@ export class WindowList extends Component { vertical: 'top', }} id="window-list-menu" - container={document.querySelector(`#${containerId} .${ns('viewer')}`)} - disableAutoFocusItem - anchorEl={anchorEl} - open={Boolean(anchorEl)} + container={container?.current} onClose={handleClose} - TransitionProps={{ - onEntering: WindowList.focus2ndListIitem, - }} + {...menuProps} > <ListSubheader role="presentation" selected={false} disabled tabIndex="-1"> {t('openWindows')} @@ -65,6 +50,7 @@ export class WindowList extends Component { windowIds.map((windowId, i) => ( <MenuItem key={windowId} + selected={windowId === focusedWindowId} onClick={(e) => { focusWindow(windowId, true); handleClose(e); }} > <ListItemText primaryTypographyProps={{ variant: 'body1' }}> @@ -81,8 +67,8 @@ export class WindowList extends Component { } WindowList.propTypes = { - anchorEl: PropTypes.object, // eslint-disable-line react/forbid-prop-types - containerId: PropTypes.string.isRequired, + container: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + focusedWindowId: PropTypes.string, focusWindow: PropTypes.func.isRequired, handleClose: PropTypes.func.isRequired, t: PropTypes.func, @@ -91,7 +77,8 @@ WindowList.propTypes = { }; WindowList.defaultProps = { - anchorEl: null, + container: null, + focusedWindowId: null, t: key => key, titles: {}, }; diff --git a/src/components/WindowListButton.js b/src/components/WindowListButton.js index 68a9bd2ac974089ddfb88609ab29b27bb4cf5954..758e227ac71202a71e2da63f7474852d608de962 100644 --- a/src/components/WindowListButton.js +++ b/src/components/WindowListButton.js @@ -1,7 +1,6 @@ import { Component } from 'react'; -import BookmarksIcon from '@material-ui/icons/BookmarksSharp'; +import BookmarksIcon from '@mui/icons-material/BookmarksSharp'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; import WindowList from '../containers/WindowList'; import MiradorMenuButton from '../containers/MiradorMenuButton'; @@ -33,7 +32,7 @@ export class WindowListButton extends Component { */ render() { const { - classes, disabled, t, windowCount, + disabled, t, windowCount, } = this.props; const { windowListAnchor } = this.state; @@ -43,24 +42,29 @@ export class WindowListButton extends Component { aria-haspopup="true" aria-label={t('listAllOpenWindows')} aria-owns={windowListAnchor ? 'window-list' : null} - className={ - classNames(classes.ctrlBtn, (windowListAnchor ? classes.ctrlBtnSelected : null)) - } + selected={Boolean(windowListAnchor)} disabled={disabled} badge - BadgeProps={{ badgeContent: windowCount, classes: { badge: classes.badge } }} - onClick={e => this.handleOpen(e)} + BadgeProps={{ + badgeContent: windowCount, + sx: { + '.MuiBadge-badge': { + paddingLeft: 1.5, + }, + }, + }} + onClick={(e) => this.handleOpen(e)} > <BookmarksIcon /> </MiradorMenuButton> {Boolean(windowListAnchor) && ( - <WindowList - anchorEl={windowListAnchor} - id="window-list" - open={Boolean(windowListAnchor)} - handleClose={this.handleClose} - /> + <WindowList + anchorEl={windowListAnchor} + id="window-list" + open={Boolean(windowListAnchor)} + handleClose={this.handleClose} + /> )} </> ); @@ -68,12 +72,10 @@ export class WindowListButton extends Component { } WindowListButton.propTypes = { - classes: PropTypes.objectOf(PropTypes.string), disabled: PropTypes.bool, t: PropTypes.func.isRequired, windowCount: PropTypes.number.isRequired, }; WindowListButton.defaultProps = { - classes: {}, disabled: false, }; diff --git a/src/components/WindowSideBar.js b/src/components/WindowSideBar.js index c6fbc6ef3f75ecd493fc2a36b9df35fe234539f6..c036a8faa4bcf92630c0acc1714d230e922bb65e 100644 --- a/src/components/WindowSideBar.js +++ b/src/components/WindowSideBar.js @@ -1,9 +1,16 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import Drawer from '@material-ui/core/Drawer'; +import { styled } from '@mui/material/styles'; +import Drawer from '@mui/material/Drawer'; import WindowSideBarButtons from '../containers/WindowSideBarButtons'; +const Root = styled(Drawer, { name: 'WindowSideBar', slot: 'root' })(({ theme }) => ({ + flexShrink: 0, + height: '100%', + order: -1000, + zIndex: theme.zIndex.appBar - 1, +})); + /** * WindowSideBar */ @@ -18,27 +25,34 @@ export class WindowSideBar extends Component { } = this.props; return ( - <Drawer + <Root variant="persistent" - className={classNames(classes.drawer)} - classes={{ paper: classNames(classes.paper) }} + className={classes.drawer} anchor={direction === 'rtl' ? 'right' : 'left'} PaperProps={{ 'aria-label': t('sidebarPanelsNavigation'), component: 'nav', - style: { height: '100%', position: 'relative' }, + sx: { + borderBlock: 0, + borderInlineStart: 0, + height: '100%', + overflowX: 'hidden', + position: 'relative', + width: 48, + }, + variant: 'outlined', }} SlideProps={{ direction: direction === 'rtl' ? 'left' : 'right', mountOnEnter: true, unmountOnExit: true }} open={sideBarOpen} > <WindowSideBarButtons windowId={windowId} /> - </Drawer> + </Root> ); } } WindowSideBar.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, + classes: PropTypes.objectOf(PropTypes.string), direction: PropTypes.string.isRequired, sideBarOpen: PropTypes.bool, t: PropTypes.func.isRequired, @@ -46,5 +60,6 @@ WindowSideBar.propTypes = { }; WindowSideBar.defaultProps = { + classes: {}, sideBarOpen: false, }; diff --git a/src/components/WindowSideBarAnnotationsPanel.js b/src/components/WindowSideBarAnnotationsPanel.js index 0a4436e7af3c3db885b44aa4cd3fa3a8d1fe52f9..34f76e9d01de8ef2357b5f5f4aa3d72fcc7b6a1d 100644 --- a/src/components/WindowSideBarAnnotationsPanel.js +++ b/src/components/WindowSideBarAnnotationsPanel.js @@ -1,8 +1,10 @@ import { createRef, Component } from 'react'; import PropTypes from 'prop-types'; +import Typography from '@mui/material/Typography'; import AnnotationSettings from '../containers/AnnotationSettings'; import CanvasAnnotations from '../containers/CanvasAnnotations'; import CompanionWindow from '../containers/CompanionWindow'; +import { CompanionWindowSection } from './CompanionWindowSection'; import ns from '../config/css-ns'; /** @@ -21,7 +23,7 @@ export class WindowSideBarAnnotationsPanel extends Component { */ render() { const { - annotationCount, classes, canvasIds, t, windowId, id, + annotationCount, canvasIds, t, windowId, id, } = this.props; return ( <CompanionWindow @@ -30,9 +32,13 @@ export class WindowSideBarAnnotationsPanel extends Component { windowId={windowId} id={id} ref={this.containerRef} - otherRef={this.containerRef} titleControls={<AnnotationSettings windowId={windowId} />} > + + <CompanionWindowSection> + <Typography component="p" variant="subtitle2">{t('showingNumAnnotations', { count: annotationCount, number: annotationCount })}</Typography> + </CompanionWindowSection> + {canvasIds.map((canvasId, index) => ( <CanvasAnnotations canvasId={canvasId} @@ -51,7 +57,6 @@ export class WindowSideBarAnnotationsPanel extends Component { WindowSideBarAnnotationsPanel.propTypes = { annotationCount: PropTypes.number.isRequired, canvasIds: PropTypes.arrayOf(PropTypes.string), - classes: PropTypes.objectOf(PropTypes.string).isRequired, id: PropTypes.string.isRequired, t: PropTypes.func, windowId: PropTypes.string.isRequired, diff --git a/src/components/WindowSideBarButtons.js b/src/components/WindowSideBarButtons.js index edff59c2eeaa234ef6937b4dc7838c567a12ef81..bba780d3d62a01e7da62d25287bc628aa4b7af42 100644 --- a/src/components/WindowSideBarButtons.js +++ b/src/components/WindowSideBarButtons.js @@ -1,21 +1,62 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Badge from '@material-ui/core/Badge'; -import Tabs from '@material-ui/core/Tabs'; -import Tab from '@material-ui/core/Tab'; -import Tooltip from '@material-ui/core/Tooltip'; -import InfoIcon from '@material-ui/icons/InfoSharp'; -import AnnotationIcon from '@material-ui/icons/CommentSharp'; -import AttributionIcon from '@material-ui/icons/CopyrightSharp'; -import LayersIcon from '@material-ui/icons/LayersSharp'; -import SearchIcon from '@material-ui/icons/SearchSharp'; +import { styled } from '@mui/material/styles'; +import Badge from '@mui/material/Badge'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Tooltip from '@mui/material/Tooltip'; +import InfoIcon from '@mui/icons-material/InfoSharp'; +import AnnotationIcon from '@mui/icons-material/CommentSharp'; +import AttributionIcon from '@mui/icons-material/CopyrightSharp'; +import LayersIcon from '@mui/icons-material/LayersSharp'; +import SearchIcon from '@mui/icons-material/SearchSharp'; import CanvasIndexIcon from './icons/CanvasIndexIcon'; +const Root = styled(Tabs, { name: 'WindowSideBarButtons', slot: 'root' })({ + '& .MuiTabs-flexContainer': { + flexDirection: 'column', + }, + '&.MuiTabs-indicator': { + display: 'none', + }, +}); + +const StyledTabButton = styled(Tab, { name: 'WindowSideBarButtons', slot: 'button' })(({ theme }) => ({ + '&.Mui-selected': { + borderRight: '2px solid', + borderRightColor: theme.palette.primary.main, + }, + '&.MuiTab-root': { + '&:active': { + backgroundColor: theme.palette.action.active, + }, + '&:focus': { + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + backgroundColor: theme.palette.action.hover, + textDecoration: 'none', + // Reset on touch devices, it doesn't add specificity + }, + '&:hover': { + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + backgroundColor: theme.palette.action.hover, + textDecoration: 'none', + // Reset on touch devices, it doesn't add specificity + }, + borderRight: '2px solid transparent', + minWidth: 'auto', + }, + fill: 'currentcolor', +})); + /** */ function TabButton({ t, value, ...tabProps }) { return ( <Tooltip title={t('openCompanionWindow', { context: value })}> - <Tab + <StyledTabButton {...tabProps} value={value} aria-label={ @@ -59,7 +100,6 @@ export class WindowSideBarButtons extends Component { */ render() { const { - classes, hasAnnotations, hasAnyAnnotations, hasAnyLayers, @@ -73,8 +113,7 @@ export class WindowSideBarButtons extends Component { } = this.props; return ( - <Tabs - classes={{ flexContainer: classes.tabsFlexContainer, indicator: classes.tabsIndicator }} + <Root value={sideBarPanel === 'closed' ? false : sideBarPanel} onChange={this.handleChange} variant="fullWidth" @@ -88,7 +127,6 @@ export class WindowSideBarButtons extends Component { <TabButton value="info" onKeyUp={this.handleKeyUp} - classes={{ root: classes.tab, selected: classes.tabSelected }} t={t} icon={(<InfoIcon />)} /> @@ -97,7 +135,6 @@ export class WindowSideBarButtons extends Component { <TabButton value="attribution" onKeyUp={this.handleKeyUp} - classes={{ root: classes.tab, selected: classes.tabSelected }} t={t} icon={(<AttributionIcon />)} /> @@ -106,7 +143,6 @@ export class WindowSideBarButtons extends Component { <TabButton value="canvas" onKeyUp={this.handleKeyUp} - classes={{ root: classes.tab, selected: classes.tabSelected }} t={t} icon={(<CanvasIndexIcon />)} /> @@ -115,10 +151,9 @@ export class WindowSideBarButtons extends Component { <TabButton value="annotations" onKeyUp={this.handleKeyUp} - classes={{ root: classes.tab, selected: classes.tabSelected }} t={t} icon={( - <Badge classes={{ badge: classes.badge }} invisible={!hasAnnotations} variant="dot"> + <Badge overlap="rectangular" color="notification" invisible={!hasAnnotations} variant="dot"> <AnnotationIcon /> </Badge> )} @@ -128,10 +163,9 @@ export class WindowSideBarButtons extends Component { <TabButton value="search" onKeyUp={this.handleKeyUp} - classes={{ root: classes.tab, selected: classes.tabSelected }} t={t} icon={( - <Badge classes={{ badge: classes.badge }} invisible={!hasSearchResults} variant="dot"> + <Badge overlap="rectangular" color="notification" invisible={!hasSearchResults} variant="dot"> <SearchIcon /> </Badge> )} @@ -141,10 +175,9 @@ export class WindowSideBarButtons extends Component { <TabButton value="layers" onKeyUp={this.handleKeyUp} - classes={{ root: classes.tab, selected: classes.tabSelected }} t={t} icon={( - <Badge classes={{ badge: classes.badge }} invisible={!hasCurrentLayers} variant="dot"> + <Badge overlap="rectangular" color="notification" invisible={!hasCurrentLayers} variant="dot"> <LayersIcon /> </Badge> )} @@ -154,21 +187,19 @@ export class WindowSideBarButtons extends Component { && PluginComponents.map(PluginComponent => ( <TabButton onKeyUp={this.handleKeyUp} - classes={{ root: classes.tab, selected: classes.tabSelected }} t={t} key={PluginComponent.value} value={PluginComponent.value} icon={<PluginComponent />} /> ))} - </Tabs> + </Root> ); } } WindowSideBarButtons.propTypes = { addCompanionWindow: PropTypes.func.isRequired, - classes: PropTypes.objectOf(PropTypes.string), hasAnnotations: PropTypes.bool, hasAnyAnnotations: PropTypes.bool, hasAnyLayers: PropTypes.bool, @@ -182,7 +213,6 @@ WindowSideBarButtons.propTypes = { }; WindowSideBarButtons.defaultProps = { - classes: {}, hasAnnotations: false, hasAnyAnnotations: false, hasAnyLayers: false, diff --git a/src/components/WindowSideBarCanvasPanel.js b/src/components/WindowSideBarCanvasPanel.js index 948fd4408336db7d2377342450229b2ac77638d4..768f8c65d5a4e5d062479d8ca9c57719e13bc5b2 100644 --- a/src/components/WindowSideBarCanvasPanel.js +++ b/src/components/WindowSideBarCanvasPanel.js @@ -1,21 +1,26 @@ import { createRef, Component } from 'react'; import PropTypes from 'prop-types'; -import Tabs from '@material-ui/core/Tabs'; -import Tab from '@material-ui/core/Tab'; -import Tooltip from '@material-ui/core/Tooltip'; -import Button from '@material-ui/core/Button'; -import ItemListIcon from '@material-ui/icons/ReorderSharp'; -import TocIcon from '@material-ui/icons/SortSharp'; -import ThumbnailListIcon from '@material-ui/icons/ViewListSharp'; -import Typography from '@material-ui/core/Typography'; -import ArrowForwardIcon from '@material-ui/icons/ArrowForwardSharp'; -import FormControl from '@material-ui/core/FormControl'; -import Select from '@material-ui/core/Select'; -import MenuItem from '@material-ui/core/MenuItem'; +import { styled } from '@mui/material/styles'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Tooltip from '@mui/material/Tooltip'; +import Button from '@mui/material/Button'; +import ItemListIcon from '@mui/icons-material/ReorderSharp'; +import TocIcon from '@mui/icons-material/SortSharp'; +import ThumbnailListIcon from '@mui/icons-material/ViewListSharp'; +import Typography from '@mui/material/Typography'; +import ArrowForwardIcon from '@mui/icons-material/ArrowForwardSharp'; +import FormControl from '@mui/material/FormControl'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; import CompanionWindow from '../containers/CompanionWindow'; import SidebarIndexList from '../containers/SidebarIndexList'; import SidebarIndexTableOfContents from '../containers/SidebarIndexTableOfContents'; +const StyledBreak = styled('div')(() => ({ + flexBasis: '100%', + height: 0, +})); /** * a panel showing the canvases for a given manifest */ @@ -57,7 +62,6 @@ export class WindowSideBarCanvasPanel extends Component { */ render() { const { - classes, collection, id, showMultipart, @@ -95,7 +99,6 @@ export class WindowSideBarCanvasPanel extends Component { id={id} windowId={windowId} ref={this.containerRef} - otherRef={this.containerRef} titleControls={( <> { @@ -107,21 +110,27 @@ export class WindowSideBarCanvasPanel extends Component { horizontal: 'left', vertical: 'bottom', }, - getContentAnchorEl: null, }} displayEmpty value={sequenceId} onChange={this.handleSequenceChange} name="sequenceId" - classes={{ select: classes.select }} - className={classes.selectEmpty} + sx={{ + '&.MuiSelect-select': { + '&:focus': { + backgroundColor: 'background.paper', + }, + }, + backgroundColor: 'background.paper', + }} + data-testid="sequence-select" > { sequences.map((s, i) => <MenuItem value={s.id} key={s.id}><Typography variant="body2">{ WindowSideBarCanvasPanel.getUseableLabel(s, i) }</Typography></MenuItem>) } </Select> </FormControl> ) } - <div className={classes.break} /> + <StyledBreak /> <Tabs value={variant} onChange={this.handleVariantChange} @@ -130,10 +139,10 @@ export class WindowSideBarCanvasPanel extends Component { textColor="primary" > {showToc && ( - <Tooltip title={t('tableOfContentsList')} value="tableOfContents"><Tab className={classes.variantTab} value="tableOfContents" aria-label={t('tableOfContentsList')} aria-controls={`tab-panel-${id}`} icon={<TocIcon style={{ transform: 'scale(-1, 1)' }} />} /></Tooltip> + <Tooltip title={t('tableOfContentsList')} value="tableOfContents"><Tab sx={{ minWidth: 'auto' }} value="tableOfContents" aria-label={t('tableOfContentsList')} aria-controls={`tab-panel-${id}`} icon={<TocIcon style={{ transform: 'scale(-1, 1)' }} />} /></Tooltip> )} - <Tooltip title={t('itemList')} value="item"><Tab className={classes.variantTab} value="item" aria-label={t('itemList')} aria-controls={`tab-panel-${id}`} icon={<ItemListIcon />} /></Tooltip> - <Tooltip title={t('thumbnailList')} value="thumbnail"><Tab className={classes.variantTab} value="thumbnail" aria-label={t('thumbnailList')} aria-controls={`tab-panel-${id}`} icon={<ThumbnailListIcon />} /></Tooltip> + <Tooltip title={t('itemList')} value="item"><Tab sx={{ minWidth: 'auto' }} value="item" aria-label={t('itemList')} aria-controls={`tab-panel-${id}`} icon={<ItemListIcon />} /></Tooltip> + <Tooltip title={t('thumbnailList')} value="thumbnail"><Tab sx={{ minWidth: 'auto' }} value="thumbnail" aria-label={t('thumbnailList')} aria-controls={`tab-panel-${id}`} icon={<ThumbnailListIcon />} /></Tooltip> </Tabs> </> )} @@ -145,7 +154,7 @@ export class WindowSideBarCanvasPanel extends Component { onClick={showMultipart} endIcon={<ArrowForwardIcon />} > - <Typography className={classes.collectionNavigationButton}> + <Typography sx={{ textTransform: 'none' }}> {WindowSideBarCanvasPanel.getUseableLabel(collection)} </Typography> </Button> @@ -158,7 +167,6 @@ export class WindowSideBarCanvasPanel extends Component { } WindowSideBarCanvasPanel.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, collection: PropTypes.object, // eslint-disable-line react/forbid-prop-types id: PropTypes.string.isRequired, sequenceId: PropTypes.string, diff --git a/src/components/WindowSideBarCollectionPanel.js b/src/components/WindowSideBarCollectionPanel.js index a6964ca77705542d3092fabc33881113f91abad8..f55a2dbdef728fbc29d65c9c04cb965644182325 100644 --- a/src/components/WindowSideBarCollectionPanel.js +++ b/src/components/WindowSideBarCollectionPanel.js @@ -1,14 +1,14 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import ListItemText from '@material-ui/core/ListItemText'; -import MenuList from '@material-ui/core/MenuList'; -import MenuItem from '@material-ui/core/MenuItem'; -import Typography from '@material-ui/core/Typography'; -import Skeleton from '@material-ui/lab/Skeleton'; -import ArrowUpwardIcon from '@material-ui/icons/ArrowUpwardSharp'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import MenuList from '@mui/material/MenuList'; +import MenuItem from '@mui/material/MenuItem'; +import Typography from '@mui/material/Typography'; +import Skeleton from '@mui/material/Skeleton'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpwardSharp'; import CompanionWindow from '../containers/CompanionWindow'; import IIIFThumbnail from '../containers/IIIFThumbnail'; @@ -20,7 +20,12 @@ function Item({ <MenuItem alignItems="flex-start" button + divider component="li" + variant="multiline" + sx={{ + paddingRight: 1, + }} {...otherProps} > { variant === 'thumbnail' && ( @@ -74,7 +79,6 @@ export class WindowSideBarCollectionPanel extends Component { render() { const { canvasNavigation, - classes, collectionPath, collection, id, @@ -114,7 +118,7 @@ export class WindowSideBarCollectionPanel extends Component { )} <Typography variant="h6"> { collection && WindowSideBarCollectionPanel.getUseableLabel(collection)} - { isFetching && <Skeleton className={classes.placeholder} variant="text" />} + { isFetching && <Skeleton variant="text" />} </Typography> </> )} @@ -123,9 +127,9 @@ export class WindowSideBarCollectionPanel extends Component { { isFetching && ( <MenuItem> <ListItemText> - <Skeleton className={classes.placeholder} variant="text" /> - <Skeleton className={classes.placeholder} variant="text" /> - <Skeleton className={classes.placeholder} variant="text" /> + <Skeleton variant="text" /> + <Skeleton variant="text" /> + <Skeleton variant="text" /> </ListItemText> </MenuItem> )} @@ -144,7 +148,6 @@ export class WindowSideBarCollectionPanel extends Component { canvasNavigation={canvasNavigation} manifest={manifest} variant={variant} - className={classes.menuItem} selected={manifestId === manifest.id} /> ); @@ -167,7 +170,6 @@ export class WindowSideBarCollectionPanel extends Component { canvasNavigation={canvasNavigation} manifest={manifest} variant={variant} - className={classes.menuItem} selected={manifestId === manifest.id} /> ); @@ -184,7 +186,6 @@ WindowSideBarCollectionPanel.propTypes = { height: PropTypes.number, width: PropTypes.number, }).isRequired, - classes: PropTypes.objectOf(PropTypes.string).isRequired, collection: PropTypes.object, // eslint-disable-line react/forbid-prop-types collectionPath: PropTypes.arrayOf(PropTypes.string), id: PropTypes.string.isRequired, diff --git a/src/components/WindowSideBarInfoPanel.js b/src/components/WindowSideBarInfoPanel.js index e2931f70e34426beac5cb635019dff157a79f99e..3261679503f289fa29caab0b4fb986e8bb963069 100644 --- a/src/components/WindowSideBarInfoPanel.js +++ b/src/components/WindowSideBarInfoPanel.js @@ -1,6 +1,7 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; import CompanionWindow from '../containers/CompanionWindow'; +import { CompanionWindowSection } from './CompanionWindowSection'; import CanvasInfo from '../containers/CanvasInfo'; import LocalePicker from '../containers/LocalePicker'; import ManifestInfo from '../containers/ManifestInfo'; @@ -21,7 +22,6 @@ export class WindowSideBarInfoPanel extends Component { windowId, id, canvasIds, - classes, collectionPath, t, locale, @@ -49,7 +49,9 @@ export class WindowSideBarInfoPanel extends Component { > { canvasIds.map((canvasId, index) => ( - <div key={canvasId} className={classes.section}> + <CompanionWindowSection + key={canvasId} + > <CanvasInfo id={id} canvasId={canvasId} @@ -57,22 +59,22 @@ export class WindowSideBarInfoPanel extends Component { totalSize={canvasIds.length} windowId={windowId} /> - </div> + </CompanionWindowSection> )) } { collectionPath.length > 0 && ( - <div className={classes.section}> + <CompanionWindowSection> <CollectionInfo id={id} windowId={windowId} /> - </div> + </CompanionWindowSection> )} - <div className={classes.section}> + <CompanionWindowSection> <ManifestInfo id={id} windowId={windowId} /> - </div> + </CompanionWindowSection> - <div className={classes.section}> + <CompanionWindowSection> <ManifestRelatedLinks id={id} windowId={windowId} /> - </div> + </CompanionWindowSection> </CompanionWindow> ); } @@ -81,7 +83,6 @@ export class WindowSideBarInfoPanel extends Component { WindowSideBarInfoPanel.propTypes = { availableLocales: PropTypes.arrayOf(PropTypes.string), canvasIds: PropTypes.arrayOf(PropTypes.string), - classes: PropTypes.objectOf(PropTypes.string), collectionPath: PropTypes.arrayOf(PropTypes.string), id: PropTypes.string.isRequired, locale: PropTypes.string, @@ -94,7 +95,6 @@ WindowSideBarInfoPanel.propTypes = { WindowSideBarInfoPanel.defaultProps = { availableLocales: [], canvasIds: [], - classes: {}, collectionPath: [], locale: '', setLocale: undefined, diff --git a/src/components/WindowThumbnailSettings.js b/src/components/WindowThumbnailSettings.js index 22b7eb57db316222e7a93a9e18b8301d138e7fb3..042355e4d01054086ec6cd21195670e166a06ffd 100644 --- a/src/components/WindowThumbnailSettings.js +++ b/src/components/WindowThumbnailSettings.js @@ -1,11 +1,25 @@ import { Component } from 'react'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import ListSubheader from '@material-ui/core/ListSubheader'; -import MenuItem from '@material-ui/core/MenuItem'; -import ThumbnailsOffIcon from '@material-ui/icons/CropDinSharp'; +import { styled } from '@mui/material/styles'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import ListSubheader from '@mui/material/ListSubheader'; +import MenuItem from '@mui/material/MenuItem'; +import ThumbnailsOffIcon from '@mui/icons-material/CropDinSharp'; import PropTypes from 'prop-types'; import ThumbnailNavigationBottomIcon from './icons/ThumbnailNavigationBottomIcon'; import ThumbnailNavigationRightIcon from './icons/ThumbnailNavigationRightIcon'; + +const ThumbnailOption = styled(MenuItem, { name: 'WindowThumbnailSettings', slot: 'option' })(({ selected, theme }) => ({ + '& .MuiFormControlLabel-label': { + borderBottom: '2px solid transparent', + ...(selected && { + borderBottomColor: theme.palette.secondary.main, + }), + }, + backgroundColor: 'transparent !important', + color: selected ? theme.palette.secondary.main : undefined, + display: 'inline-block', +})); + /** * */ @@ -34,56 +48,53 @@ export class WindowThumbnailSettings extends Component { */ render() { const { - classes, handleClose, t, thumbnailNavigationPosition, direction, + handleClose, t, thumbnailNavigationPosition, direction, } = this.props; return ( <> - <ListSubheader role="presentation" disableSticky tabIndex="-1">{t('thumbnails')}</ListSubheader> + <ListSubheader role="presentation" disableSticky tabIndex={-1}>{t('thumbnails')}</ListSubheader> - <MenuItem className={classes.MenuItem} onClick={() => { this.handleChange('off'); handleClose(); }}> + <ThumbnailOption selected={thumbnailNavigationPosition === 'off'} onClick={() => { this.handleChange('off'); handleClose(); }}> <FormControlLabel value="off" - classes={{ label: thumbnailNavigationPosition === 'off' ? classes.selectedLabel : classes.label }} control={ - <ThumbnailsOffIcon color={thumbnailNavigationPosition === 'off' ? 'secondary' : undefined} /> + <ThumbnailsOffIcon color={thumbnailNavigationPosition === 'off' ? 'secondary' : undefined} fill="currentcolor" /> } label={t('off')} labelPlacement="bottom" /> - </MenuItem> - <MenuItem className={classes.MenuItem} onClick={() => { this.handleChange('far-bottom'); handleClose(); }}> + </ThumbnailOption> + <ThumbnailOption selected={thumbnailNavigationPosition === 'far-bottom'} onClick={() => { this.handleChange('far-bottom'); handleClose(); }}> <FormControlLabel value="far-bottom" - classes={{ label: thumbnailNavigationPosition === 'far-bottom' ? classes.selectedLabel : classes.label }} control={ - <ThumbnailNavigationBottomIcon color={thumbnailNavigationPosition === 'far-bottom' ? 'secondary' : undefined} /> + <ThumbnailNavigationBottomIcon color={thumbnailNavigationPosition === 'far-bottom' ? 'secondary' : undefined} fill="currentcolor" /> } label={t('bottom')} labelPlacement="bottom" /> - </MenuItem> - <MenuItem className={classes.MenuItem} onClick={() => { this.handleChange('far-right'); handleClose(); }}> + </ThumbnailOption> + <ThumbnailOption selected={thumbnailNavigationPosition === 'far-right'} onClick={() => { this.handleChange('far-right'); handleClose(); }}> <FormControlLabel value="far-right" - classes={{ label: thumbnailNavigationPosition === 'far-right' ? classes.selectedLabel : classes.label }} control={( <ThumbnailNavigationRightIcon color={thumbnailNavigationPosition === 'far-right' ? 'secondary' : undefined} + fill="currentcolor" style={direction === 'rtl' ? { transform: 'rotate(180deg)' } : {}} /> )} label={t('right')} labelPlacement="bottom" /> - </MenuItem> + </ThumbnailOption> </> ); } } WindowThumbnailSettings.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, direction: PropTypes.string.isRequired, handleClose: PropTypes.func, setWindowThumbnailPosition: PropTypes.func.isRequired, diff --git a/src/components/WindowTopBar.js b/src/components/WindowTopBar.js index 31795b9b9ce376ba27fc3a21c2d87a7bd6d50479..abc496c2ccc00d24b239dcd978d61303e4ef137d 100644 --- a/src/components/WindowTopBar.js +++ b/src/components/WindowTopBar.js @@ -1,9 +1,10 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import MenuIcon from '@material-ui/icons/MenuSharp'; -import CloseIcon from '@material-ui/icons/CloseSharp'; -import Toolbar from '@material-ui/core/Toolbar'; -import AppBar from '@material-ui/core/AppBar'; +import { styled } from '@mui/material/styles'; +import MenuIcon from '@mui/icons-material/MenuSharp'; +import CloseIcon from '@mui/icons-material/CloseSharp'; +import Toolbar from '@mui/material/Toolbar'; +import AppBar from '@mui/material/AppBar'; import classNames from 'classnames'; import WindowTopMenuButton from '../containers/WindowTopMenuButton'; import WindowTopBarPluginArea from '../containers/WindowTopBarPluginArea'; @@ -15,6 +16,22 @@ import WindowMaxIcon from './icons/WindowMaxIcon'; import WindowMinIcon from './icons/WindowMinIcon'; import ns from '../config/css-ns'; +const Root = styled(AppBar, { name: 'WindowTopBar', slot: 'root' })(() => ({ + zIndex: 1100, +})); + +const StyledToolbar = styled(Toolbar, { name: 'WindowTopBar', slot: 'toolbar' })(({ ownerState, theme }) => ({ + backgroundColor: theme.palette.shades?.main, + borderTop: '2px solid', + borderTopColor: ownerState?.focused ? theme.palette.primary.main : 'transparent', + minHeight: 32, + paddingLeft: theme.spacing(0.5), + paddingRight: theme.spacing(0.5), + ...(ownerState?.windowDraggable && { + cursor: 'move', + }), +})); + /** * WindowTopBar */ @@ -25,65 +42,60 @@ export class WindowTopBar extends Component { */ render() { const { - removeWindow, windowId, classes, toggleWindowSideBar, t, windowDraggable, - maximizeWindow, maximized, minimizeWindow, focused, allowClose, allowMaximize, + removeWindow, windowId, toggleWindowSideBar, t, + maximizeWindow, maximized, minimizeWindow, allowClose, allowMaximize, focusWindow, allowFullscreen, allowTopMenuButton, allowWindowSideBar, } = this.props; return ( - <AppBar position="relative" color="default"> - <nav aria-label={t('windowNavigation')}> - <Toolbar - disableGutters - onMouseDown={focusWindow} - className={classNames( - classes.windowTopBarStyle, - windowDraggable ? classes.windowTopBarStyleDraggable : null, - focused ? classes.focused : null, - ns('window-top-bar'), - )} - variant="dense" - > - {allowWindowSideBar && ( - <MiradorMenuButton - aria-label={t('toggleWindowSideBar')} - onClick={toggleWindowSideBar} - > - <MenuIcon /> - </MiradorMenuButton> - )} - <WindowTopBarTitle - windowId={windowId} - /> - {allowTopMenuButton && ( - <WindowTopMenuButton className={ns('window-menu-btn')} windowId={windowId} /> - )} - <WindowTopBarPluginArea windowId={windowId} /> - <WindowTopBarPluginMenu windowId={windowId} /> - {allowMaximize && ( - <MiradorMenuButton - aria-label={(maximized ? t('minimizeWindow') : t('maximizeWindow'))} - className={ns('window-maximize')} - onClick={(maximized ? minimizeWindow : maximizeWindow)} - > - {(maximized ? <WindowMinIcon /> : <WindowMaxIcon />)} - </MiradorMenuButton> - )} - {allowFullscreen && ( - <FullScreenButton /> - )} - {allowClose && ( - <MiradorMenuButton - aria-label={t('closeWindow')} - className={ns('window-close')} - onClick={removeWindow} - > - <CloseIcon /> - </MiradorMenuButton> - )} - </Toolbar> - </nav> - </AppBar> + <Root component="nav" aria-label={t('windowNavigation')} position="relative" color="default" enableColorOnDark> + <StyledToolbar + disableGutters + onMouseDown={focusWindow} + ownerState={this.props} + className={classNames(ns('window-top-bar'))} + variant="dense" + > + {allowWindowSideBar && ( + <MiradorMenuButton + aria-label={t('toggleWindowSideBar')} + onClick={toggleWindowSideBar} + className={ns('window-menu-btn')} + > + <MenuIcon /> + </MiradorMenuButton> + )} + <WindowTopBarTitle + windowId={windowId} + /> + {allowTopMenuButton && ( + <WindowTopMenuButton windowId={windowId} className={ns('window-menu-btn')} /> + )} + <WindowTopBarPluginArea windowId={windowId} /> + <WindowTopBarPluginMenu windowId={windowId} /> + {allowMaximize && ( + <MiradorMenuButton + aria-label={(maximized ? t('minimizeWindow') : t('maximizeWindow'))} + className={classNames(ns('window-maximize'), ns('window-menu-btn'))} + onClick={(maximized ? minimizeWindow : maximizeWindow)} + > + {(maximized ? <WindowMinIcon /> : <WindowMaxIcon />)} + </MiradorMenuButton> + )} + {allowFullscreen && ( + <FullScreenButton className={ns('window-menu-btn')} /> + )} + {allowClose && ( + <MiradorMenuButton + aria-label={t('closeWindow')} + className={classNames(ns('window-close'), ns('window-menu-btn'))} + onClick={removeWindow} + > + <CloseIcon /> + </MiradorMenuButton> + )} + </StyledToolbar> + </Root> ); } } @@ -94,8 +106,7 @@ WindowTopBar.propTypes = { allowMaximize: PropTypes.bool, allowTopMenuButton: PropTypes.bool, allowWindowSideBar: PropTypes.bool, - classes: PropTypes.objectOf(PropTypes.string).isRequired, - focused: PropTypes.bool, + focused: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types focusWindow: PropTypes.func, maximized: PropTypes.bool, maximizeWindow: PropTypes.func, @@ -103,7 +114,7 @@ WindowTopBar.propTypes = { removeWindow: PropTypes.func.isRequired, t: PropTypes.func, toggleWindowSideBar: PropTypes.func.isRequired, - windowDraggable: PropTypes.bool, + windowDraggable: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types windowId: PropTypes.string.isRequired, }; diff --git a/src/components/WindowTopBarPluginMenu.js b/src/components/WindowTopBarPluginMenu.js index a44ddf9d155ae364ee60bf17f0eda41c92fc62cc..972cbf544b6c32b1aa87f2d8fcfff1e770ef0b54 100644 --- a/src/components/WindowTopBarPluginMenu.js +++ b/src/components/WindowTopBarPluginMenu.js @@ -1,10 +1,9 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import MoreVertIcon from '@material-ui/icons/MoreVertSharp'; -import Menu from '@material-ui/core/Menu'; +import MoreVertIcon from '@mui/icons-material/MoreVertSharp'; +import Menu from '@mui/material/Menu'; import MiradorMenuButton from '../containers/MiradorMenuButton'; import { PluginHook } from './PluginHook'; -import ns from '../config/css-ns'; /** * @@ -17,6 +16,7 @@ export class WindowTopBarPluginMenu extends Component { super(props); this.state = { anchorEl: null, + open: false, }; this.handleMenuClick = this.handleMenuClick.bind(this); this.handleMenuClose = this.handleMenuClose.bind(this); @@ -28,6 +28,7 @@ export class WindowTopBarPluginMenu extends Component { handleMenuClick(event) { this.setState({ anchorEl: event.currentTarget, + open: true, }); } @@ -37,6 +38,7 @@ export class WindowTopBarPluginMenu extends Component { handleMenuClose() { this.setState({ anchorEl: null, + open: false, }); } @@ -45,10 +47,10 @@ export class WindowTopBarPluginMenu extends Component { */ render() { const { - classes, containerId, PluginComponents, t, windowId, menuIcon, + container, PluginComponents, t, windowId, menuIcon, } = this.props; - const { anchorEl } = this.state; - + const { anchorEl, open } = this.state; + const windowPluginMenuId = `window-plugin-menu_${windowId}`; if (!PluginComponents || PluginComponents.length === 0) return null; return ( @@ -56,16 +58,16 @@ export class WindowTopBarPluginMenu extends Component { <MiradorMenuButton aria-haspopup="true" aria-label={t('windowPluginMenu')} - aria-owns={anchorEl ? `window-plugin-menu_${windowId}` : undefined} - className={anchorEl ? classes.ctrlBtnSelected : null} + aria-owns={open ? windowPluginMenuId : undefined} + selected={open} onClick={this.handleMenuClick} > {menuIcon} </MiradorMenuButton> <Menu - id={`window-plugin-menu_${windowId}`} - container={document.querySelector(`#${containerId} .${ns('viewer')}`)} + id={windowPluginMenuId} + container={container?.current} anchorEl={anchorEl} anchorOrigin={{ horizontal: 'right', @@ -75,8 +77,7 @@ export class WindowTopBarPluginMenu extends Component { horizontal: 'right', vertical: 'top', }} - getContentAnchorEl={null} - open={Boolean(anchorEl)} + open={open} onClose={() => this.handleMenuClose()} > <PluginHook handleClose={() => this.handleMenuClose()} {...this.props} /> @@ -87,11 +88,10 @@ export class WindowTopBarPluginMenu extends Component { } WindowTopBarPluginMenu.propTypes = { - classes: PropTypes.shape({ - ctrlBtnSelected: PropTypes.string, - }), - containerId: PropTypes.string.isRequired, + anchorEl: PropTypes.object, // eslint-disable-line react/forbid-prop-types + container: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), menuIcon: PropTypes.element, + open: PropTypes.bool, PluginComponents: PropTypes.arrayOf( PropTypes.node, ), @@ -100,7 +100,9 @@ WindowTopBarPluginMenu.propTypes = { }; WindowTopBarPluginMenu.defaultProps = { - classes: {}, + anchorEl: null, + container: null, menuIcon: <MoreVertIcon />, + open: false, PluginComponents: [], }; diff --git a/src/components/WindowTopBarTitle.js b/src/components/WindowTopBarTitle.js index b47f00c2c2e03433cf198724553503e38fecb5ec..5c91250e12861c7e2a4426e214fdc5499e663bba 100644 --- a/src/components/WindowTopBarTitle.js +++ b/src/components/WindowTopBarTitle.js @@ -1,9 +1,21 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Typography from '@material-ui/core/Typography'; -import Skeleton from '@material-ui/lab/Skeleton'; -import ErrorIcon from '@material-ui/icons/ErrorOutlineSharp'; +import { styled } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; +import Skeleton from '@mui/material/Skeleton'; +import ErrorIcon from '@mui/icons-material/ErrorOutlineSharp'; +const StyledTitleTypography = styled(TitleTypography)(({ theme }) => ({ + ...theme.typography.h6, + flexGrow: 1, + paddingLeft: theme.spacing(0.5), +})); + +const StyledTitle = styled('div')(({ theme }) => ({ + ...theme.typography.h6, + flexGrow: 1, + paddingLeft: theme.spacing(0.5), +})); /** */ function TitleTypography({ children, ...props }) { return ( @@ -27,32 +39,32 @@ export class WindowTopBarTitle extends Component { */ render() { const { - classes, error, hideWindowTitle, isFetching, manifestTitle, + error, hideWindowTitle, isFetching, manifestTitle, } = this.props; let title = null; if (isFetching) { title = ( - <TitleTypography className={classes.title}> + <StyledTitleTypography> <Skeleton variant="text" /> - </TitleTypography> + </StyledTitleTypography> ); } else if (error) { title = ( <> <ErrorIcon color="error" /> - <TitleTypography color="textSecondary" className={classes.title}> + <StyledTitleTypography color="textSecondary"> {error} - </TitleTypography> + </StyledTitleTypography> </> ); } else if (hideWindowTitle) { - title = (<div className={classes.title} />); + title = (<StyledTitle />); } else { title = ( - <TitleTypography className={classes.title}> + <StyledTitleTypography> {manifestTitle} - </TitleTypography> + </StyledTitleTypography> ); } return title; @@ -60,7 +72,6 @@ export class WindowTopBarTitle extends Component { } WindowTopBarTitle.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, error: PropTypes.string, hideWindowTitle: PropTypes.bool, isFetching: PropTypes.bool, diff --git a/src/components/WindowTopMenu.js b/src/components/WindowTopMenu.js index 99e89f0d2138a48268d2cd35af89ea7076335fd1..feb45377bd37aafa51e484b40216faf69c2d2340 100644 --- a/src/components/WindowTopMenu.js +++ b/src/components/WindowTopMenu.js @@ -1,18 +1,17 @@ import { Component } from 'react'; -import Menu from '@material-ui/core//Menu'; -import ListSubheader from '@material-ui/core/ListSubheader'; +import Menu from '@mui/material//Menu'; +import ListSubheader from '@mui/material/ListSubheader'; import PropTypes from 'prop-types'; import WindowThumbnailSettings from '../containers/WindowThumbnailSettings'; import WindowViewSettings from '../containers/WindowViewSettings'; import { PluginHook } from './PluginHook'; -import ns from '../config/css-ns'; /** Renders plugins */ function PluginHookWithHeader(props) { const { PluginComponents, t } = props; // eslint-disable-line react/prop-types return PluginComponents ? ( <> - <ListSubheader role="presentation" disableSticky tabIndex="-1">{t('windowPluginButtons')}</ListSubheader> + <ListSubheader role="presentation" disableSticky tabIndex={-1}>{t('windowPluginButtons')}</ListSubheader> <PluginHook {...props} /> </> ) : null; @@ -27,15 +26,13 @@ export class WindowTopMenu extends Component { */ render() { const { - containerId, handleClose, anchorEl, showThumbnailNavigationSettings, - toggleDraggingEnabled, windowId, + container, handleClose, showThumbnailNavigationSettings, + toggleDraggingEnabled, windowId, anchorEl, open, } = this.props; return ( <Menu - id={`window-menu_${windowId}`} - container={document.querySelector(`#${containerId} .${ns('viewer')}`)} - anchorEl={anchorEl} + container={container?.current} anchorOrigin={{ horizontal: 'right', vertical: 'bottom', @@ -44,14 +41,14 @@ export class WindowTopMenu extends Component { horizontal: 'right', vertical: 'top', }} - getContentAnchorEl={null} - open={Boolean(anchorEl)} onClose={handleClose} TransitionProps={{ onEntering: toggleDraggingEnabled, onExit: toggleDraggingEnabled, }} orientation="horizontal" + anchorEl={anchorEl} + open={open} > <WindowViewSettings windowId={windowId} handleClose={handleClose} /> {showThumbnailNavigationSettings @@ -64,8 +61,9 @@ export class WindowTopMenu extends Component { WindowTopMenu.propTypes = { anchorEl: PropTypes.object, // eslint-disable-line react/forbid-prop-types - containerId: PropTypes.string.isRequired, + container: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), handleClose: PropTypes.func.isRequired, + open: PropTypes.bool, showThumbnailNavigationSettings: PropTypes.bool, toggleDraggingEnabled: PropTypes.func.isRequired, windowId: PropTypes.string.isRequired, @@ -73,5 +71,7 @@ WindowTopMenu.propTypes = { WindowTopMenu.defaultProps = { anchorEl: null, + container: null, + open: false, showThumbnailNavigationSettings: true, }; diff --git a/src/components/WindowTopMenuButton.js b/src/components/WindowTopMenuButton.js index 8fee77043e56336a5ce4a92a005b52972593f0b5..e48bd84d2c3c5aa0d6fbbab209a1d1f28be262d8 100644 --- a/src/components/WindowTopMenuButton.js +++ b/src/components/WindowTopMenuButton.js @@ -14,6 +14,7 @@ export class WindowTopMenuButton extends Component { super(props); this.state = { anchorEl: null, + open: false, }; this.handleMenuClick = this.handleMenuClick.bind(this); this.handleMenuClose = this.handleMenuClose.bind(this); @@ -24,7 +25,8 @@ export class WindowTopMenuButton extends Component { */ handleMenuClick(event) { this.setState({ - anchorEl: event.currentTarget, + anchorEl: event.target, + open: true, }); } @@ -34,6 +36,7 @@ export class WindowTopMenuButton extends Component { handleMenuClose() { this.setState({ anchorEl: null, + open: false, }); } @@ -42,16 +45,19 @@ export class WindowTopMenuButton extends Component { * @return */ render() { - const { classes, t, windowId } = this.props; - const { anchorEl } = this.state; - + const { + classes, t, windowId, + } = this.props; + const { open, anchorEl } = this.state; + const menuId = `window-menu_${windowId}`; return ( <> <MiradorMenuButton aria-haspopup="true" aria-label={t('windowMenu')} - aria-owns={anchorEl ? `window-menu_${windowId}` : undefined} - className={anchorEl ? classes.ctrlBtnSelected : null} + aria-owns={open ? menuId : undefined} + className={open ? classes.ctrlBtnSelected : undefined} + selected={open} onClick={this.handleMenuClick} > <WindowOptionsIcon /> @@ -60,6 +66,8 @@ export class WindowTopMenuButton extends Component { windowId={windowId} anchorEl={anchorEl} handleClose={this.handleMenuClose} + id={menuId} + open={open} /> </> ); @@ -67,11 +75,12 @@ export class WindowTopMenuButton extends Component { } WindowTopMenuButton.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, + classes: PropTypes.objectOf(PropTypes.string), t: PropTypes.func, windowId: PropTypes.string.isRequired, }; WindowTopMenuButton.defaultProps = { + classes: {}, t: key => key, }; diff --git a/src/components/WindowViewSettings.js b/src/components/WindowViewSettings.js index 90c56b823269cf733b54c88132b4cb732467883e..e693b2ffcdc62caab53200c3178d6597257169ea 100644 --- a/src/components/WindowViewSettings.js +++ b/src/components/WindowViewSettings.js @@ -1,13 +1,26 @@ import { Component } from 'react'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import MenuItem from '@material-ui/core/MenuItem'; -import ListSubheader from '@material-ui/core/ListSubheader'; -import SingleIcon from '@material-ui/icons/CropOriginalSharp'; -import ScrollViewIcon from '@material-ui/icons/ViewColumn'; +import { styled } from '@mui/material/styles'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import MenuItem from '@mui/material/MenuItem'; +import ListSubheader from '@mui/material/ListSubheader'; +import SingleIcon from '@mui/icons-material/CropOriginalSharp'; +import ScrollViewIcon from '@mui/icons-material/ViewColumn'; import PropTypes from 'prop-types'; import BookViewIcon from './icons/BookViewIcon'; import GalleryViewIcon from './icons/GalleryViewIcon'; +const ViewOption = styled(MenuItem, { name: 'WindowViewSettings', slot: 'option' })(({ selected, theme }) => ({ + '& .MuiFormControlLabel-label': { + borderBottom: '2px solid transparent', + ...(selected && { + borderBottomColor: theme.palette.secondary.main, + }), + }, + backgroundColor: 'transparent !important', + color: selected ? theme.palette.secondary.main : undefined, + display: 'inline-block', +})); + /** * */ @@ -36,7 +49,7 @@ export class WindowViewSettings extends Component { */ render() { const { - classes, handleClose, t, windowViewType, viewTypes, + handleClose, t, windowViewType, viewTypes, } = this.props; const iconMap = { @@ -49,26 +62,25 @@ export class WindowViewSettings extends Component { /** Suspiciously similar to a component, yet if it is invoked through JSX none of the click handlers work? */ const menuItem = ({ value, Icon }) => ( - <MenuItem + <ViewOption + selected={windowViewType === value} key={value} - className={classes.MenuItem} autoFocus={windowViewType === value} onClick={() => { this.handleChange(value); handleClose(); }} > <FormControlLabel value={value} - classes={{ label: windowViewType === value ? classes.selectedLabel : classes.label }} - control={<Icon color={windowViewType === value ? 'secondary' : undefined} />} + control={<Icon fill="currentcolor" color={windowViewType === value ? 'secondary' : undefined} />} label={t(value)} labelPlacement="bottom" /> - </MenuItem> + </ViewOption> ); if (viewTypes.length === 0) return null; return ( <> - <ListSubheader role="presentation" disableSticky tabIndex="-1">{t('view')}</ListSubheader> + <ListSubheader role="presentation" disableSticky tabIndex={-1}>{t('view')}</ListSubheader> { viewTypes.map(value => menuItem({ Icon: iconMap[value], value })) } </> ); @@ -76,7 +88,6 @@ export class WindowViewSettings extends Component { } WindowViewSettings.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, handleClose: PropTypes.func, setWindowViewType: PropTypes.func.isRequired, t: PropTypes.func, diff --git a/src/components/Workspace.js b/src/components/Workspace.js index f99f61d4e4351078b811d874dba828aca7d7ad6f..8963e182326b98de4e1e60800af67ba4e2c20faf 100644 --- a/src/components/Workspace.js +++ b/src/components/Workspace.js @@ -1,14 +1,36 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; import classNames from 'classnames'; -import Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; +import Grid from '@mui/material/Grid'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import { visuallyHidden } from '@mui/utils'; import Window from '../containers/Window'; import WorkspaceMosaic from '../containers/WorkspaceMosaic'; import WorkspaceElastic from '../containers/WorkspaceElastic'; import ns from '../config/css-ns'; import { IIIFDropTarget } from './IIIFDropTarget'; +const Root = styled('div', { name: 'Workspace', slot: 'root' })(({ ownerState, theme }) => ({ + '@media (min-width: 600px)': { + ...(ownerState.isWorkspaceControlPanelVisible && { + paddingLeft: theme.spacing(8.5), + paddingTop: 0, + }), + }, + ...(ownerState.isWorkspaceControlPanelVisible && { + paddingTop: theme.spacing(9.25), + }), + bottom: 0, + left: 0, + margin: 0, + overflow: 'hidden', + position: 'absolute', + right: 0, + top: 0, +})); + /** * Represents a work area that contains any number of windows * @memberof Workspace @@ -62,26 +84,32 @@ export class Workspace extends Component { const { t } = this.props; return ( - <Grid - alignItems="center" - container + <Paper style={{ height: '100%', }} > <Grid - xs={12} - item + alignItems="center" + container + style={{ + height: '100%', + }} > - <Typography - variant="h1" - component="div" - align="center" + <Grid + xs={12} + item > - {t('welcome')} - </Typography> + <Typography + variant="h1" + component="div" + align="center" + > + {t('welcome')} + </Typography> + </Grid> </Grid> - </Grid> + </Paper> ); } @@ -107,23 +135,22 @@ export class Workspace extends Component { * render */ render() { - const { classes, isWorkspaceControlPanelVisible, t } = this.props; + const { isWorkspaceControlPanelVisible, t } = this.props; return ( <IIIFDropTarget onDrop={this.handleDrop}> - <div + <Root + ownerState={this.props} className={ classNames( ns('workspace-viewport'), (isWorkspaceControlPanelVisible && ns('workspace-with-control-panel')), - (isWorkspaceControlPanelVisible && classes.workspaceWithControlPanel), - classes.workspaceViewport, ) } > - <Typography variant="srOnly" component="h1">{t('miradorViewer')}</Typography> + <Typography style={visuallyHidden} component="h1">{t('miradorViewer')}</Typography> {this.workspaceByType()} - </div> + </Root> </IIIFDropTarget> ); } @@ -132,7 +159,6 @@ export class Workspace extends Component { Workspace.propTypes = { addWindow: PropTypes.func, allowNewWindows: PropTypes.bool, - classes: PropTypes.objectOf(PropTypes.string).isRequired, isWorkspaceControlPanelVisible: PropTypes.bool.isRequired, maximizedWindowIds: PropTypes.arrayOf(PropTypes.string), t: PropTypes.func.isRequired, diff --git a/src/components/WorkspaceAdd.js b/src/components/WorkspaceAdd.js index 172c8d3474970c769e39084dbde0bad3b43c846f..cced34789c9e064d3fdc0d71deb7297847360639 100644 --- a/src/components/WorkspaceAdd.js +++ b/src/components/WorkspaceAdd.js @@ -1,16 +1,18 @@ import { createRef, Component } from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; import classNames from 'classnames'; -import AddIcon from '@material-ui/icons/AddSharp'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMoreSharp'; -import AppBar from '@material-ui/core/AppBar'; -import Drawer from '@material-ui/core/Drawer'; -import Grid from '@material-ui/core/Grid'; -import Fab from '@material-ui/core/Fab'; -import List from '@material-ui/core/List'; -import Paper from '@material-ui/core/Paper'; -import Toolbar from '@material-ui/core/Toolbar'; -import Typography from '@material-ui/core/Typography'; +import AddIcon from '@mui/icons-material/AddSharp'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMoreSharp'; +import AppBar from '@mui/material/AppBar'; +import Drawer from '@mui/material/Drawer'; +import Grid from '@mui/material/Grid'; +import Fab from '@mui/material/Fab'; +import List from '@mui/material/List'; +import Paper from '@mui/material/Paper'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import { visuallyHidden } from '@mui/utils'; import ns from '../config/css-ns'; import ManifestForm from '../containers/ManifestForm'; import ManifestListItem from '../containers/ManifestListItem'; @@ -18,6 +20,24 @@ import MiradorMenuButton from '../containers/MiradorMenuButton'; import { IIIFDropTarget } from './IIIFDropTarget'; import { PluginHook } from './PluginHook'; +const StyledWorkspaceAdd = styled('div')(() => ({ + boxSizing: 'border-box', + height: '100%', + overflowX: 'hidden', + overflowY: 'auto', + paddingTop: 68, + // injection order matters + // eslint-disable-next-line sort-keys + '@media (min-width: 600px)': { + paddingLeft: 68, + paddingTop: 0, + }, +})); + +const StyledMiradorMenuButton = styled(MiradorMenuButton)(() => ({ + marginLeft: -12, + marginRight: 20, +})); /** * An area for managing manifests and adding them to workspace * @memberof Workspace @@ -75,7 +95,7 @@ export class WorkspaceAdd extends Component { */ render() { const { - catalog, setWorkspaceAddVisibility, t, classes, + catalog, setWorkspaceAddVisibility, t, } = this.props; const { addResourcesOpen } = this.state; @@ -91,7 +111,7 @@ export class WorkspaceAdd extends Component { return ( <IIIFDropTarget onDrop={this.handleDrop}> - <div ref={this.ref} className={classNames(ns('workspace-add'), classes.workspaceAdd)}> + <StyledWorkspaceAdd ref={this.ref} className={classNames(ns('workspace-add'))}> {catalog.length < 1 ? ( <Grid alignItems="center" @@ -114,8 +134,8 @@ export class WorkspaceAdd extends Component { </Grid> </Grid> ) : ( - <Paper className={classes.list}> - <Typography variant="srOnly" component="h1">{t('miradorResources')}</Typography> + <Paper sx={{ margin: 2 }}> + <Typography style={visuallyHidden} component="h1">{t('miradorResources')}</Typography> <PluginHook {...this.props} /> <List disablePadding> {manifestList} @@ -125,7 +145,12 @@ export class WorkspaceAdd extends Component { <Fab variant="extended" disabled={addResourcesOpen} - className={classNames(classes.fab, ns('add-resource-button'))} + sx={(theme) => ({ + bottom: theme.spacing(2), + position: 'absolute', + right: theme.spacing(2), + })} + className={classNames(ns('add-resource-button'))} color="primary" onClick={() => (this.setAddResourcesVisibility(true))} > @@ -134,10 +159,18 @@ export class WorkspaceAdd extends Component { </Fab> <Drawer - className={classNames({ - [classes.displayNone]: !addResourcesOpen, + sx={theme => ({ + '.MuiDrawer-paper': { + borderTop: '0', + left: '0', + [theme.breakpoints.up('sm')]: { + left: '65px', + }, + }, + ...(!addResourcesOpen && { + display: 'none', + }), })} - classes={{ paper: classes.paper }} variant="persistent" anchor="bottom" open={addResourcesOpen} @@ -148,18 +181,25 @@ export class WorkspaceAdd extends Component { }} > <Paper - className={classes.form} + sx={{ + left: '0', + marginTop: 6, + paddingBottom: 2, + paddingLeft: { sm: 3, xs: 2 }, + paddingRight: { sm: 3, xs: 2 }, + paddingTop: 2, + right: '0', + }} > - <AppBar position="absolute" color="primary" onClick={() => (this.setAddResourcesVisibility(false))}> + <AppBar position="absolute" color="primary" enableColorOnDark onClick={() => (this.setAddResourcesVisibility(false))}> <Toolbar variant="dense"> - <MiradorMenuButton + <StyledMiradorMenuButton aria-label={t('closeAddResourceForm')} - className={classes.menuButton} color="inherit" > <ExpandMoreIcon /> - </MiradorMenuButton> - <Typography variant="h2" noWrap color="inherit" className={classes.typographyBody}> + </StyledMiradorMenuButton> + <Typography variant="h2" noWrap color="inherit" sx={{ flexGrow: 1 }}> {t('addResource')} </Typography> </Toolbar> @@ -171,7 +211,7 @@ export class WorkspaceAdd extends Component { /> </Paper> </Drawer> - </div> + </StyledWorkspaceAdd> </IIIFDropTarget> ); } @@ -183,7 +223,6 @@ WorkspaceAdd.propTypes = { manifestId: PropTypes.string.isRequired, provider: PropTypes.string, })), - classes: PropTypes.objectOf(PropTypes.string), setWorkspaceAddVisibility: PropTypes.func.isRequired, t: PropTypes.func, }; @@ -191,6 +230,5 @@ WorkspaceAdd.propTypes = { WorkspaceAdd.defaultProps = { addResource: () => {}, catalog: [], - classes: {}, t: key => key, }; diff --git a/src/components/WorkspaceAddButton.js b/src/components/WorkspaceAddButton.js index b80ec8136911b0e8a34a16b74ab6c748de3ce6f2..b50679226e4082364041ce4eb77c73b56d1579ca 100644 --- a/src/components/WorkspaceAddButton.js +++ b/src/components/WorkspaceAddButton.js @@ -1,9 +1,14 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import Fab from '@material-ui/core/Fab'; -import Tooltip from '@material-ui/core/Tooltip'; -import AddIcon from '@material-ui/icons/AddSharp'; -import CloseIcon from '@material-ui/icons/CloseSharp'; +import Fab from '@mui/material/Fab'; +import Tooltip from '@mui/material/Tooltip'; +import AddIcon from '@mui/icons-material/AddSharp'; +import CloseIcon from '@mui/icons-material/CloseSharp'; +import { styled } from '@mui/material/styles'; + +const Root = styled(Fab, { name: 'WorkspaceAddButton', slot: 'root' })(({ theme }) => ({ + marginBottom: theme.spacing(1), +})); /** */ @@ -14,22 +19,19 @@ export class WorkspaceAddButton extends Component { */ render() { const { - classes, t, setWorkspaceAddVisibility, isWorkspaceAddVisible, useExtendedFab, + t, setWorkspaceAddVisibility, isWorkspaceAddVisible, useExtendedFab, } = this.props; return ( <Tooltip title={isWorkspaceAddVisible ? t('closeAddResourceMenu') : t('addResource')}> - <Fab + <Root size="medium" color="primary" id="addBtn" - disableRipple aria-label={ isWorkspaceAddVisible ? t('closeAddResourceMenu') : ((useExtendedFab && t('startHere')) || t('addResource')) } - className={classes.fab} - classes={{ primary: classes.fabPrimary, secondary: classes.fabSecondary }} variant={useExtendedFab ? 'extended' : 'circular'} onClick={() => { setWorkspaceAddVisibility(!isWorkspaceAddVisible); }} > @@ -39,14 +41,13 @@ export class WorkspaceAddButton extends Component { : <AddIcon /> } { useExtendedFab && t('startHere') } - </Fab> + </Root> </Tooltip> ); } } WorkspaceAddButton.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, isWorkspaceAddVisible: PropTypes.bool, setWorkspaceAddVisibility: PropTypes.func.isRequired, t: PropTypes.func, diff --git a/src/components/WorkspaceArea.js b/src/components/WorkspaceArea.js index 9650158cb7cf49d9a386ceba2bc3fa8fea4f0cb9..886118f616052b1d36396b19def21e3144d240e9 100644 --- a/src/components/WorkspaceArea.js +++ b/src/components/WorkspaceArea.js @@ -1,6 +1,6 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; +import { styled, lighten, darken } from '@mui/material/styles'; import ErrorDialog from '../containers/ErrorDialog'; import WorkspaceControlPanel from '../containers/WorkspaceControlPanel'; import Workspace from '../containers/Workspace'; @@ -8,6 +8,20 @@ import WorkspaceAdd from '../containers/WorkspaceAdd'; import BackgroundPluginArea from '../containers/BackgroundPluginArea'; import ns from '../config/css-ns'; +const StyledMain = styled('main')(({ theme }) => { + const getBackgroundColor = theme.palette.mode === 'light' ? darken : lighten; + + return { + background: getBackgroundColor(theme.palette.grey.A200, 0.1), + bottom: 0, + left: 0, + overflow: 'hidden', + position: 'absolute', + right: 0, + top: 0, + }; +}); + /** * This is the top level Mirador component. * @prop {Object} manifests @@ -19,7 +33,7 @@ export class WorkspaceArea extends Component { */ render() { const { - classes, + areaRef, controlPanelVariant, isWorkspaceAddVisible, isWorkspaceControlPanelVisible, @@ -33,10 +47,11 @@ export class WorkspaceArea extends Component { isWorkspaceControlPanelVisible && <WorkspaceControlPanel variant={controlPanelVariant} /> } - <main - className={classNames(classes.viewer, ns('viewer'))} + <StyledMain + className={ns('viewer')} lang={lang} aria-label={t('workspace')} + {...(areaRef ? { ref: areaRef } : {})} > { isWorkspaceAddVisible @@ -45,14 +60,14 @@ export class WorkspaceArea extends Component { } <ErrorDialog /> <BackgroundPluginArea /> - </main> + </StyledMain> </> ); } } WorkspaceArea.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, + areaRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), controlPanelVariant: PropTypes.string, isWorkspaceAddVisible: PropTypes.bool, isWorkspaceControlPanelVisible: PropTypes.bool.isRequired, @@ -61,6 +76,7 @@ WorkspaceArea.propTypes = { }; WorkspaceArea.defaultProps = { + areaRef: null, controlPanelVariant: undefined, isWorkspaceAddVisible: false, lang: undefined, diff --git a/src/components/WorkspaceControlPanel.js b/src/components/WorkspaceControlPanel.js index f6703ff883a55a904ffe2842f477f400cdb57f19..ddcd967284ca3a0b4000c4288316f3fe22c12e37 100644 --- a/src/components/WorkspaceControlPanel.js +++ b/src/components/WorkspaceControlPanel.js @@ -1,13 +1,60 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; import classNames from 'classnames'; -import AppBar from '@material-ui/core/AppBar'; -import Toolbar from '@material-ui/core/Toolbar'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; import WorkspaceAddButton from '../containers/WorkspaceAddButton'; import WorkspaceControlPanelButtons from '../containers/WorkspaceControlPanelButtons'; import Branding from '../containers/Branding'; import ns from '../config/css-ns'; +const Root = styled(AppBar, { name: 'WorkspaceControlPanel', slot: 'root' })(({ ownerState, theme }) => ({ + display: 'flex', + height: 64, + padding: theme.spacing(1), + paddingBottom: 0, + [theme.breakpoints.up('sm')]: { + height: '100%', + left: 0, + right: 'auto', + width: ownerState.variant === 'wide' ? 'auto' : 64, + }, + ...(ownerState.variant === 'wide' && { + width: 'auto', + }), +})); + +const StyledToolbar = styled(Toolbar, { name: 'WorkspaceControlPanel', slot: 'toolbar' })(({ theme }) => ({ + display: 'flex', + flexGrow: 1, + justifyContent: 'space-between', + [theme.breakpoints.up('sm')]: { + flexDirection: 'column', + justifyContent: 'flex-start', + minHeight: 0, + }, +})); + +const StyledWorkspaceButtons = styled('div', { name: 'WorkspaceControlPanel', slot: 'buttonArea' })(({ theme }) => ({ + [theme.breakpoints.up('sm')]: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + marginBottom: theme.spacing(1), + marginTop: theme.spacing(1), + }, +})); + +const StyledBranding = styled(Branding, { name: 'WorkspaceControlPanel', slot: 'branding' })(({ theme }) => ({ + [theme.breakpoints.up('xs')]: { + display: 'none', + }, + [theme.breakpoints.up('sm')]: { + display: 'flex', + }, +})); + /** * Provides the panel responsible for controlling the entire workspace */ @@ -17,29 +64,32 @@ export class WorkspaceControlPanel extends Component { * @return {String} - HTML markup for the component */ render() { - const { classes, t, variant } = this.props; + const { t, variant } = this.props; return ( - <AppBar - className={classNames(classes.root, ns('workspace-control-panel'), variant === 'wide' ? classes.wide : null)} + <Root + ownerState={this.props} + className={classNames(ns('workspace-control-panel'))} color="default" + enableColorOnDark position="absolute" component="nav" aria-label={t('workspaceNavigation')} > - <Toolbar disableGutters className={classes.toolbar}> + <StyledToolbar + disableGutters + > <WorkspaceAddButton /> - <div className={classes.workspaceButtons}> + <StyledWorkspaceButtons> <WorkspaceControlPanelButtons /> - </div> - </Toolbar> - <Branding className={classes.branding} t={t} variant={variant} /> - </AppBar> + </StyledWorkspaceButtons> + </StyledToolbar> + <StyledBranding t={t} variant={variant} /> + </Root> ); } } WorkspaceControlPanel.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, t: PropTypes.func.isRequired, variant: PropTypes.oneOf(['default', 'wide']), }; diff --git a/src/components/WorkspaceControlPanelButtons.js b/src/components/WorkspaceControlPanelButtons.js index b07adfc54298955b08cbfc1ffd60299bbad125f9..34cbf840ea9a1810a90e13bbbae9776b684cfea5 100644 --- a/src/components/WorkspaceControlPanelButtons.js +++ b/src/components/WorkspaceControlPanelButtons.js @@ -1,5 +1,4 @@ import { Component } from 'react'; -import PropTypes from 'prop-types'; import FullScreenButton from '../containers/FullScreenButton'; import WorkspaceMenuButton from '../containers/WorkspaceMenuButton'; import WorkspaceOptionsButton from '../containers/WorkspaceOptionsButton'; @@ -16,23 +15,14 @@ export class WorkspaceControlPanelButtons extends Component { * @return {type} description */ render() { - const { classes } = this.props; return ( <> <WindowListButton /> <WorkspaceMenuButton /> <WorkspaceOptionsButton /> - <FullScreenButton className={classes.ctrlBtn} /> + <FullScreenButton /> <PluginHook {...this.props} /> </> ); } } - -WorkspaceControlPanelButtons.propTypes = { - classes: PropTypes.objectOf(PropTypes.string), -}; - -WorkspaceControlPanelButtons.defaultProps = { - classes: {}, -}; diff --git a/src/components/WorkspaceDialog.js b/src/components/WorkspaceDialog.js new file mode 100644 index 0000000000000000000000000000000000000000..0928db6502f527924127ff0032422de6fa8aed1b --- /dev/null +++ b/src/components/WorkspaceDialog.js @@ -0,0 +1,13 @@ +import { + Dialog, +} from '@mui/material'; +import { styled } from '@mui/material/styles'; + +export const WorkspaceDialog = styled(Dialog, { name: 'WorkspaceDialog', slot: 'root' })(({ theme, variant }) => ({ + '& .MuiDialogTitle-root': theme.unstable_sx({ typography: 'h2' }), + ...(variant === 'menu' && { + '& .MuiDialogContent-root': { + padding: 0, + }, + }), +})); diff --git a/src/components/WorkspaceElastic.js b/src/components/WorkspaceElastic.js index a79219ac1ba1697359168476f0ea816acb9ebee2..c910f0d30166df12a43a1ae5df8456d44bc53e24 100644 --- a/src/components/WorkspaceElastic.js +++ b/src/components/WorkspaceElastic.js @@ -1,11 +1,23 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; import { Rnd } from 'react-rnd'; import ResizeObserver from 'react-resize-observer'; -import classNames from 'classnames'; import WorkspaceElasticWindow from '../containers/WorkspaceElasticWindow'; import ns from '../config/css-ns'; +const StyledRnd = styled(Rnd)({ + boxSizing: 'border-box', + margin: 0, + position: 'absolute', + transitionDuration: '.7s', + // order matters + // eslint-disable-next-line sort-keys + '& .react-draggable-dragging': { + transitionDuration: 'unset', + }, +}); + /** * Represents a work area that contains any number of windows * @memberof Workspace @@ -16,7 +28,6 @@ class WorkspaceElastic extends Component { */ render() { const { - classes, workspace, elasticLayout, setWorkspaceViewportDimensions, @@ -30,10 +41,11 @@ class WorkspaceElastic extends Component { return ( <div style={{ height: '100%', position: 'relative', width: '100%' }}> <ResizeObserver + onReflow={() => {}} onResize={(rect) => { setWorkspaceViewportDimensions(rect); }} /> - <Rnd + <StyledRnd size={{ height: workspace.height, width: workspace.width, @@ -55,7 +67,7 @@ class WorkspaceElastic extends Component { setWorkspaceViewportPosition({ x: -1 * d.x - offsetX, y: -1 * d.y - offsetY }); }} cancel={`.${ns('window')}`} - className={classNames(classes.workspace, ns('workspace'))} + className={ns('workspace')} disableDragging={!workspace.draggingEnabled} > { @@ -66,14 +78,13 @@ class WorkspaceElastic extends Component { /> )) } - </Rnd> + </StyledRnd> </div> ); } } WorkspaceElastic.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, elasticLayout: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types setWorkspaceViewportDimensions: PropTypes.func.isRequired, setWorkspaceViewportPosition: PropTypes.func.isRequired, diff --git a/src/components/WorkspaceElasticWindow.js b/src/components/WorkspaceElasticWindow.js index 169b5d5fde9422f9160ceac401d7f6fa844b7f9e..dea8c87b6ad8318300aae46219ba30f763da09af 100644 --- a/src/components/WorkspaceElasticWindow.js +++ b/src/components/WorkspaceElasticWindow.js @@ -1,9 +1,14 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; import { Rnd } from 'react-rnd'; import Window from '../containers/Window'; import ns from '../config/css-ns'; +const StyledRnd = styled(Rnd)(({ focused, theme }) => ({ + zIndex: focused ? theme.zIndex.modal - 1 : 'auto', +})); + /** * Represents a work area that contains any number of windows * @memberof Workspace @@ -17,6 +22,7 @@ class WorkspaceElasticWindow extends Component { classes, companionWindowDimensions, focused, + focusWindow, layout, workspace, updateElasticWindowLayout, @@ -26,7 +32,9 @@ class WorkspaceElasticWindow extends Component { const offsetY = workspace.height / 2; return ( - <Rnd + <StyledRnd + focused={focused} + className={focused ? classes.focused : undefined} key={`${layout.windowId}-${workspace.id}`} size={{ height: layout.height + companionWindowDimensions.height, @@ -40,6 +48,7 @@ class WorkspaceElasticWindow extends Component { { x: d.x - offsetX, y: d.y - offsetY }, ); }} + onDragStart={focusWindow} onResize={(e, direction, ref, delta, position) => { updateElasticWindowLayout(layout.windowId, { height: Number.parseInt(ref.style.height, 10) - companionWindowDimensions.height, @@ -49,14 +58,12 @@ class WorkspaceElasticWindow extends Component { }); }} dragHandleClassName={ns('window-top-bar')} - className={ - focused ? classes.focused : null - } + cancel={`.${ns('window-menu-btn')}`} > <Window windowId={layout.windowId} /> - </Rnd> + </StyledRnd> ); } } @@ -68,6 +75,7 @@ WorkspaceElasticWindow.propTypes = { width: PropTypes.number, }), focused: PropTypes.bool, + focusWindow: PropTypes.func, layout: PropTypes.shape({ height: PropTypes.number, id: PropTypes.string, @@ -84,6 +92,7 @@ WorkspaceElasticWindow.defaultProps = { classes: {}, companionWindowDimensions: { height: 0, width: 0 }, focused: false, + focusWindow: () => {}, }; export default WorkspaceElasticWindow; diff --git a/src/components/WorkspaceExport.js b/src/components/WorkspaceExport.js index 0ee2f72a822ea38692cc2b0d681854dee05b696a..d6a7962bd3f262c099ad38869a0a1f859bf536ed 100644 --- a/src/components/WorkspaceExport.js +++ b/src/components/WorkspaceExport.js @@ -1,19 +1,19 @@ import { Component } from 'react'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import DialogContent from '@material-ui/core/DialogContent'; -import Typography from '@material-ui/core/Typography'; -import Snackbar from '@material-ui/core/Snackbar'; -import IconButton from '@material-ui/core/IconButton'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import CloseIcon from '@material-ui/icons/Close'; -import Accordion from '@material-ui/core/Accordion'; -import AccordionSummary from '@material-ui/core/AccordionSummary'; -import AccordionDetails from '@material-ui/core/AccordionDetails'; +import Button from '@mui/material/Button'; +import DialogActions from '@mui/material/DialogActions'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import Typography from '@mui/material/Typography'; +import Snackbar from '@mui/material/Snackbar'; +import IconButton from '@mui/material/IconButton'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import CloseIcon from '@mui/icons-material/Close'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import AccordionDetails from '@mui/material/AccordionDetails'; import PropTypes from 'prop-types'; import { CopyToClipboard } from 'react-copy-to-clipboard'; +import { WorkspaceDialog } from './WorkspaceDialog'; /** */ @@ -54,7 +54,7 @@ export class WorkspaceExport extends Component { */ render() { const { - children, classes, container, open, t, + children, container, open, t, } = this.props; const { copied } = this.state; @@ -79,8 +79,8 @@ export class WorkspaceExport extends Component { } return ( - <Dialog - id="workspace-settings" + <WorkspaceDialog + id="workspace-export" container={container} open={open} onClose={this.handleClose} @@ -88,19 +88,18 @@ export class WorkspaceExport extends Component { fullWidth maxWidth="sm" > - <DialogTitle id="form-dialog-title" disableTypography> - <Typography variant="h2">{t('downloadExport')}</Typography> + <DialogTitle id="form-dialog-title"> + {t('downloadExport')} </DialogTitle> <DialogContent> - <Accordion elevation={0}> + <Accordion elevation={2}> <AccordionSummary - classes={{ root: classes.accordionTitle }} expandIcon={<ExpandMoreIcon />} > <Typography variant="h4">{t('viewWorkspaceConfiguration')}</Typography> </AccordionSummary> - <AccordionDetails> + <AccordionDetails sx={{ overflow: 'scroll' }}> {children} <pre> {this.exportedState()} @@ -118,14 +117,13 @@ export class WorkspaceExport extends Component { <Button variant="contained" color="primary">{t('copy')}</Button> </CopyToClipboard> </DialogActions> - </Dialog> + </WorkspaceDialog> ); } } WorkspaceExport.propTypes = { children: PropTypes.node, - classes: PropTypes.objectOf(PropTypes.string), container: PropTypes.object, // eslint-disable-line react/forbid-prop-types exportableState: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types handleClose: PropTypes.func.isRequired, @@ -135,7 +133,6 @@ WorkspaceExport.propTypes = { WorkspaceExport.defaultProps = { children: null, - classes: {}, container: null, open: false, t: key => key, diff --git a/src/components/WorkspaceImport.js b/src/components/WorkspaceImport.js index 7adf4838d5137f93a7f81e1c823dfaa685aa384a..3a50c482871fa14b6be568a083e83edea289c799 100644 --- a/src/components/WorkspaceImport.js +++ b/src/components/WorkspaceImport.js @@ -1,13 +1,12 @@ import { Component } from 'react'; -import Dialog from '@material-ui/core/Dialog'; -import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogTitle from '@mui/material/DialogTitle'; import PropTypes from 'prop-types'; import { DialogActions, TextField, - Typography, -} from '@material-ui/core'; -import Button from '@material-ui/core/Button'; +} from '@mui/material'; +import Button from '@mui/material/Button'; +import { WorkspaceDialog } from './WorkspaceDialog'; import ScrollIndicatedDialogContent from '../containers/ScrollIndicatedDialogContent'; /** @@ -65,17 +64,16 @@ export class WorkspaceImport extends Component { } = this.props; return ( - <Dialog + <WorkspaceDialog aria-labelledby="workspace-import-title" id="workspace-import" - onEscapeKeyDown={handleClose} onClose={handleClose} open={open} fullWidth maxWidth="sm" > - <DialogTitle id="workspace-import-title" disableTypography> - <Typography variant="h2">{t('importWorkspace')}</Typography> + <DialogTitle id="workspace-import-title"> + {t('importWorkspace')} </DialogTitle> <ScrollIndicatedDialogContent> <TextField @@ -83,21 +81,25 @@ export class WorkspaceImport extends Component { id="workspace-import-input" multiline onChange={this.handleChange} - rows="15" + minRows={15} variant="filled" - inputProps={{ autoFocus: 'autofocus', className: classes.textInput }} + sx={{ + '& .MuiInputBase-input': { fontFamily: 'monospace' }, + width: '100%', + }} + inputProps={{ autoFocus: 'autofocus' }} helperText={t('importWorkspaceHint')} /> </ScrollIndicatedDialogContent> <DialogActions> - <Button className={classes.cancelBtn} onClick={handleClose}> + <Button onClick={handleClose}> {t('cancel')} </Button> <Button color="primary" onClick={this.handleImportConfig} variant="contained"> {t('import')} </Button> </DialogActions> - </Dialog> + </WorkspaceDialog> ); } } diff --git a/src/components/WorkspaceMenu.js b/src/components/WorkspaceMenu.js index 4bc9963f964c10b60ce383718d34c7330b63eada..c6e9a191a60a864ef82114775e7d57a413c3e394 100644 --- a/src/components/WorkspaceMenu.js +++ b/src/components/WorkspaceMenu.js @@ -1,12 +1,11 @@ import { Component } from 'react'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import Typography from '@material-ui/core/Typography'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import Typography from '@mui/material/Typography'; import PropTypes from 'prop-types'; import LanguageSettings from '../containers/LanguageSettings'; import { NestedMenu } from './NestedMenu'; import WorkspaceSelectionDialog from '../containers/WorkspaceSelectionDialog'; -import ns from '../config/css-ns'; import ChangeThemeDialog from '../containers/ChangeThemeDialog'; import { PluginHook } from './PluginHook'; @@ -65,14 +64,17 @@ export class WorkspaceMenu extends Component { */ render() { const { - containerId, + container, handleClose, - anchorEl, showThemePicker, isWorkspaceAddVisible, t, showZoomControls, + toggleZoomControls, + ...rest } = this.props; + const menuProps = { ...rest }; + delete menuProps.tReady; const { changeTheme, @@ -80,14 +82,10 @@ export class WorkspaceMenu extends Component { workspaceSelection, } = this.state; - const container = document.querySelector(`#${containerId} .${ns('viewer')}`); - return ( <> <Menu - id="workspace-menu" - container={container} - anchorEl={anchorEl} + container={container?.current} anchorOrigin={{ horizontal: 'right', vertical: 'top', @@ -96,8 +94,8 @@ export class WorkspaceMenu extends Component { horizontal: 'left', vertical: 'top', }} - open={Boolean(anchorEl)} onClose={handleClose} + {...menuProps} > <MenuItem aria-haspopup="true" @@ -133,7 +131,7 @@ export class WorkspaceMenu extends Component { </Menu> {Boolean(changeTheme.open) && ( <ChangeThemeDialog - container={container} + container={container?.current} handleClose={this.handleMenuItemClose('changeTheme')} open={Boolean(changeTheme.open)} /> @@ -141,7 +139,7 @@ export class WorkspaceMenu extends Component { {Boolean(workspaceSelection.open) && ( <WorkspaceSelectionDialog open={Boolean(workspaceSelection.open)} - container={container} + container={container?.current} handleClose={this.handleMenuItemClose('workspaceSelection')} /> )} @@ -151,8 +149,7 @@ export class WorkspaceMenu extends Component { } WorkspaceMenu.propTypes = { - anchorEl: PropTypes.object, // eslint-disable-line react/forbid-prop-types - containerId: PropTypes.string.isRequired, + container: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), handleClose: PropTypes.func.isRequired, isWorkspaceAddVisible: PropTypes.bool, showThemePicker: PropTypes.bool, @@ -162,7 +159,7 @@ WorkspaceMenu.propTypes = { }; WorkspaceMenu.defaultProps = { - anchorEl: null, + container: null, isWorkspaceAddVisible: false, showThemePicker: false, showZoomControls: false, diff --git a/src/components/WorkspaceMenuButton.js b/src/components/WorkspaceMenuButton.js index b1e4fe3dcd84df88eef085eb5f59cf066fdca3c5..1ff00786511d9794a21b77b5230d4682a110fbe5 100644 --- a/src/components/WorkspaceMenuButton.js +++ b/src/components/WorkspaceMenuButton.js @@ -1,7 +1,6 @@ import { Component } from 'react'; -import SettingsIcon from '@material-ui/icons/SettingsSharp'; +import SettingsIcon from '@mui/icons-material/SettingsSharp'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; import WorkspaceMenu from '../containers/WorkspaceMenu'; import MiradorMenuButton from '../containers/MiradorMenuButton'; @@ -15,6 +14,7 @@ export class WorkspaceMenuButton extends Component { super(props); this.state = { anchorEl: null, + open: false, }; this.handleMenuClick = this.handleMenuClick.bind(this); this.handleMenuClose = this.handleMenuClose.bind(this); @@ -26,6 +26,7 @@ export class WorkspaceMenuButton extends Component { handleMenuClick(event) { this.setState({ anchorEl: event.currentTarget, + open: true, }); } @@ -35,6 +36,7 @@ export class WorkspaceMenuButton extends Component { handleMenuClose() { this.setState({ anchorEl: null, + open: false, }); } @@ -43,16 +45,16 @@ export class WorkspaceMenuButton extends Component { * @return */ render() { - const { classes, t } = this.props; - const { anchorEl } = this.state; + const { t } = this.props; + const { anchorEl, open } = this.state; return ( <> <MiradorMenuButton aria-haspopup="true" aria-label={t('workspaceMenu')} - aria-owns={anchorEl ? 'workspace-menu' : undefined} - className={classNames(classes.ctrlBtn, (anchorEl ? classes.ctrlBtnSelected : null))} + aria-owns={open ? 'workspace-menu' : undefined} + selected={open} id="menuBtn" onClick={this.handleMenuClick} > @@ -60,7 +62,9 @@ export class WorkspaceMenuButton extends Component { </MiradorMenuButton> <WorkspaceMenu anchorEl={anchorEl} + id="workspace-menu" handleClose={this.handleMenuClose} + open={open} /> </> ); @@ -68,7 +72,6 @@ export class WorkspaceMenuButton extends Component { } WorkspaceMenuButton.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, t: PropTypes.func, }; diff --git a/src/components/WorkspaceMosaic.js b/src/components/WorkspaceMosaic.js index 620a8f6015b4356cab42fbf76df151347f8a5668..0ac4faa5725b56d46a6691b3f1ba2c853b1d2191 100644 --- a/src/components/WorkspaceMosaic.js +++ b/src/components/WorkspaceMosaic.js @@ -1,7 +1,10 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; +import GlobalStyles from '@mui/material/GlobalStyles'; +import { DndContext } from 'react-dnd'; import { - MosaicWithoutDragDropContext, MosaicWindow, getLeaves, createBalancedTreeFromLeaves, + Mosaic, MosaicWindow, getLeaves, createBalancedTreeFromLeaves, } from 'react-mosaic-component'; import difference from 'lodash/difference'; import isEqual from 'lodash/isEqual'; @@ -9,6 +12,22 @@ import classNames from 'classnames'; import MosaicRenderPreview from '../containers/MosaicRenderPreview'; import Window from '../containers/Window'; import MosaicLayout from '../lib/MosaicLayout'; +import globalReactMosaicStyles from '../styles/react-mosaic-component'; + +const StyledMosaic = styled(Mosaic)({ + '& .mosaic-preview': { + boxShadow: 'none', + }, + '& .mosaic-tile': { + boxShadow: '0 1px 3px 0 rgba(0, 0, 0, .2), 0 1px 1px 0 rgba(0, 0, 0, .2), 0 2px 1px -1px rgba(0, 0, 0, .2)', + }, + '& .mosaic-window': { + boxShadow: 'none', + }, + '& .mosaic-window-toolbar': { + display: 'none !important', + }, +}); /** * Represents a work area that contains any number of windows @@ -142,21 +161,28 @@ export class WorkspaceMosaic extends Component { /** */ render() { - const { layout, classes } = this.props; + const { layout } = this.props; return ( - <MosaicWithoutDragDropContext - renderTile={this.tileRenderer} - initialValue={layout || this.determineWorkspaceLayout()} - onChange={this.mosaicChange} - className={classNames('mirador-mosaic', classes.root)} - zeroStateView={this.zeroStateView} - /> + <DndContext.Consumer> + {(ctx) => ( + <> + <GlobalStyles styles={{ ...globalReactMosaicStyles }} /> + <StyledMosaic + dragAndDropManager={ctx.dragDropManager} + renderTile={this.tileRenderer} + initialValue={layout || this.determineWorkspaceLayout()} + onChange={this.mosaicChange} + className={classNames('mirador-mosaic')} + zeroStateView={this.zeroStateView} + /> + </> + )} + </DndContext.Consumer> ); } } WorkspaceMosaic.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, layout: PropTypes.oneOfType( [PropTypes.object, PropTypes.string], ), // eslint-disable-line react/forbid-prop-types diff --git a/src/components/WorkspaceOptionsButton.js b/src/components/WorkspaceOptionsButton.js index f82691e4304faa3d1d2c6ec4b725578d57c8a5bd..ad549551f2fe45cc99475b120ed0f6f57c317274 100644 --- a/src/components/WorkspaceOptionsButton.js +++ b/src/components/WorkspaceOptionsButton.js @@ -1,7 +1,6 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import MoreHorizontalIcon from '@material-ui/icons/MoreHorizSharp'; +import MoreHorizontalIcon from '@mui/icons-material/MoreHorizSharp'; import MiradorMenuButton from '../containers/MiradorMenuButton'; import WorkspaceOptionsMenu from '../containers/WorkspaceOptionsMenu'; @@ -14,7 +13,10 @@ export class WorkspaceOptionsButton extends Component { */ constructor(props) { super(props); - this.state = { anchorEl: null }; + this.state = { + anchorEl: null, + open: false, + }; this.handleMenuClick = this.handleMenuClick.bind(this); this.handleMenuClose = this.handleMenuClose.bind(this); } @@ -25,6 +27,7 @@ export class WorkspaceOptionsButton extends Component { handleMenuClick(event) { this.setState({ anchorEl: event.currentTarget, + open: true, }); } @@ -34,6 +37,7 @@ export class WorkspaceOptionsButton extends Component { handleMenuClose() { this.setState({ anchorEl: null, + open: false, }); } @@ -41,24 +45,22 @@ export class WorkspaceOptionsButton extends Component { * Returns the rendered component */ render() { - const { classes, t } = this.props; - const { anchorEl } = this.state; + const { t } = this.props; + const { anchorEl, open } = this.state; return ( <> <MiradorMenuButton aria-label={t('workspaceOptions')} - className={ - classNames(classes.ctrlBtn, (anchorEl ? classes.ctrlBtnSelected : null)) - } onClick={this.handleMenuClick} + selected={open} > <MoreHorizontalIcon /> </MiradorMenuButton> - <WorkspaceOptionsMenu anchorEl={anchorEl} handleClose={this.handleMenuClose} + open={open} /> </> ); @@ -66,6 +68,5 @@ export class WorkspaceOptionsButton extends Component { } WorkspaceOptionsButton.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, t: PropTypes.func.isRequired, }; diff --git a/src/components/WorkspaceOptionsMenu.js b/src/components/WorkspaceOptionsMenu.js index bcd482d1e4030aafc91281997538695eeded6f56..75aac27a94e95a337614cd8126e09cd823524530 100644 --- a/src/components/WorkspaceOptionsMenu.js +++ b/src/components/WorkspaceOptionsMenu.js @@ -1,15 +1,14 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; -import ImportIcon from '@material-ui/icons/Input'; -import SaveAltIcon from '@material-ui/icons/SaveAltSharp'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import Typography from '@material-ui/core/Typography'; +import ImportIcon from '@mui/icons-material/Input'; +import SaveAltIcon from '@mui/icons-material/SaveAltSharp'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import Typography from '@mui/material/Typography'; import WorkspaceExport from '../containers/WorkspaceExport'; import WorkspaceImport from '../containers/WorkspaceImport'; import { PluginHook } from './PluginHook'; -import ns from '../config/css-ns'; /** * WorkspaceOptionsMenu ~ the menu for workspace options such as import/export @@ -55,16 +54,15 @@ export class WorkspaceOptionsMenu extends Component { */ render() { const { - anchorEl, containerId, handleClose, t, + anchorEl, container, handleClose, t, open, } = this.props; const { exportWorkspace, importWorkspace } = this.state; - const container = document.querySelector(`#${containerId} .${ns('viewer')}`); return ( <> <Menu id="workspace-options-menu" - container={container} + container={container?.current} anchorEl={anchorEl} anchorOrigin={{ horizontal: 'right', @@ -74,7 +72,7 @@ export class WorkspaceOptionsMenu extends Component { horizontal: 'left', vertical: 'top', }} - open={Boolean(anchorEl)} + open={open} onClose={handleClose} > <MenuItem @@ -103,14 +101,14 @@ export class WorkspaceOptionsMenu extends Component { {Boolean(exportWorkspace.open) && ( <WorkspaceExport open={Boolean(exportWorkspace.open)} - container={container} + container={container?.current} handleClose={this.handleMenuItemClose('exportWorkspace')} /> )} {Boolean(importWorkspace.open) && ( <WorkspaceImport open={Boolean(importWorkspace.open)} - container={container} + container={container?.current} handleClose={this.handleMenuItemClose('importWorkspace')} /> )} @@ -121,11 +119,14 @@ export class WorkspaceOptionsMenu extends Component { WorkspaceOptionsMenu.propTypes = { anchorEl: PropTypes.object, // eslint-disable-line react/forbid-prop-types - containerId: PropTypes.string.isRequired, + container: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), handleClose: PropTypes.func.isRequired, + open: PropTypes.bool, t: PropTypes.func.isRequired, }; WorkspaceOptionsMenu.defaultProps = { anchorEl: null, + container: null, + open: false, }; diff --git a/src/components/WorkspaceSelectionDialog.js b/src/components/WorkspaceSelectionDialog.js index 839c227efe396c1fde096e045a6e6e2528d8acd8..e606ef872fe8974f1ceba61c6e5c3f53afdd7922 100644 --- a/src/components/WorkspaceSelectionDialog.js +++ b/src/components/WorkspaceSelectionDialog.js @@ -1,33 +1,26 @@ import { Component } from 'react'; -import Dialog from '@material-ui/core/Dialog'; -import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogTitle from '@mui/material/DialogTitle'; import { Card, CardContent, MenuList, MenuItem, Typography, -} from '@material-ui/core'; +} from '@mui/material'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; +import { WorkspaceDialog } from './WorkspaceDialog'; import WorkspaceTypeElasticIcon from './icons/WorkspaceTypeElasticIcon'; import WorkspaceTypeMosaicIcon from './icons/WorkspaceTypeMosaicIcon'; import ScrollIndicatedDialogContent from '../containers/ScrollIndicatedDialogContent'; +const StyledDetails = styled('div')(() => ({ + display: 'flex', + flexDirection: 'column', +})); /** */ export class WorkspaceSelectionDialog extends Component { - /** - * Set the initial focus when the dialog enters - * Find the selected item by using the current workspace type - * in a selector on the value attribute (which we need to set) - */ - static setInitialFocus(dialogElement, workspaceType) { - const selectedListItem = dialogElement.querySelectorAll(`li[value="${workspaceType}"]`); - if (!selectedListItem || selectedListItem.length === 0) return; - - selectedListItem[0].focus(); - } - /** * constructor */ @@ -54,81 +47,133 @@ export class WorkspaceSelectionDialog extends Component { */ render() { const { - classes, container, handleClose, open, children, t, workspaceType, + container, handleClose, open, children, t, workspaceType, } = this.props; return ( - <Dialog + <WorkspaceDialog aria-labelledby="workspace-selection-dialog-title" container={container} id="workspace-selection-dialog" onClose={handleClose} - onEntered={dialog => WorkspaceSelectionDialog.setInitialFocus(dialog, workspaceType)} - onEscapeKeyDown={handleClose} open={open} > - <DialogTitle id="workspace-selection-dialog-title" disableTypography> - <Typography variant="h2">{t('workspaceSelectionTitle')}</Typography> + <DialogTitle id="workspace-selection-dialog-title"> + {t('workspaceSelectionTitle')} </DialogTitle> <ScrollIndicatedDialogContent> {children} <MenuList - classes={{ root: classes.list }} + sx={{ + '&active': { + outline: 'none', + }, + '&focus': { + outline: 'none', + }, + outline: 'none', + }} selected={workspaceType} + autoFocusItem > <MenuItem - className={classes.menuItem} + sx={{ + height: 'auto', + overflow: 'auto', + whiteSpace: 'inherit', + }} onClick={() => this.handleWorkspaceTypeChange('elastic')} selected={workspaceType === 'elastic'} value="elastic" > - <Card className={classes.card}> + <Card sx={{ + backgroundColor: 'transparent', + borderRadius: '0', + boxShadow: '0 0 transparent', + display: 'flex', + }} + > <WorkspaceTypeElasticIcon - className={classes.svgIcon} + sx={{ + flexShrink: 0, + height: '90px', + width: '120px', + }} viewBox="0 0 120 90" /> - <div className={classes.details}> + <StyledDetails> <CardContent - classes={{ root: classes.root }} - className={classes.content} + sx={{ + '&.MuiCardContent-root': { + '&:last-child': { + paddingBottom: '12px', + }, + paddingBottom: 0, + paddingTop: 0, + textAlign: 'left', + }, + flex: '1 0 auto', + }} > - <Typography className={classes.headline} component="p" variant="h3">{t('elastic')}</Typography> + <Typography sx={{ paddingBottom: '6px' }} component="p" variant="h3">{t('elastic')}</Typography> <Typography variant="body1">{t('elasticDescription')}</Typography> </CardContent> - </div> + </StyledDetails> </Card> </MenuItem> <MenuItem - className={classes.menuItem} + sx={{ + height: 'auto', + overflow: 'auto', + whiteSpace: 'inherit', + }} onClick={() => this.handleWorkspaceTypeChange('mosaic')} selected={workspaceType === 'mosaic'} value="mosaic" > - <Card className={classes.card}> + <Card sx={{ + backgroundColor: 'transparent', + borderRadius: '0', + boxShadow: '0 0 transparent', + display: 'flex', + }} + > <WorkspaceTypeMosaicIcon - className={classes.svgIcon} + sx={{ + flexShrink: 0, + height: '90px', + width: '120px', + }} viewBox="0 0 120 90" /> - <div className={classes.details}> + <StyledDetails> <CardContent - className={classes.content} - classes={{ root: classes.root }} + sx={{ + '&.MuiCardContent-root': { + '&:last-child': { + paddingBottom: '12px', + }, + paddingBottom: 0, + paddingTop: 0, + textAlign: 'left', + }, + flex: '1 0 auto', + }} > - <Typography className={classes.headline} component="p" variant="h3">{t('mosaic')}</Typography> + <Typography sx={{ paddingBottom: '6px' }} component="p" variant="h3">{t('mosaic')}</Typography> <Typography variant="body1">{t('mosaicDescription')}</Typography> </CardContent> - </div> + </StyledDetails> </Card> </MenuItem> </MenuList> </ScrollIndicatedDialogContent> - </Dialog> + </WorkspaceDialog> ); } } WorkspaceSelectionDialog.propTypes = { children: PropTypes.node, - classes: PropTypes.objectOf(PropTypes.string).isRequired, container: PropTypes.object, // eslint-disable-line react/forbid-prop-types handleClose: PropTypes.func.isRequired, open: PropTypes.bool, diff --git a/src/components/ZoomControls.js b/src/components/ZoomControls.js index 133c9950a8d8facc4598ac95c74a034d4e1d1e7a..19dc0002291173fe8c4de3467cd11a91b4519b77 100644 --- a/src/components/ZoomControls.js +++ b/src/components/ZoomControls.js @@ -1,10 +1,17 @@ import { Component } from 'react'; -import AddCircleIcon from '@material-ui/icons/AddCircleOutlineSharp'; -import RemoveCircleIcon from '@material-ui/icons/RemoveCircleOutlineSharp'; +import AddCircleIcon from '@mui/icons-material/AddCircleOutlineSharp'; +import RemoveCircleIcon from '@mui/icons-material/RemoveCircleOutlineSharp'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import RestoreZoomIcon from './icons/RestoreZoomIcon'; import MiradorMenuButton from '../containers/MiradorMenuButton'; +const StyledZoomControlsWrapper = styled('div')({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', +}); + /** */ export class ZoomControls extends Component { @@ -46,17 +53,11 @@ export class ZoomControls extends Component { */ render() { const { - displayDivider, showZoomControls, classes, t, zoomToWorld, + t, zoomToWorld, } = this.props; - if (!showZoomControls) { - return ( - <> - </> - ); - } return ( - <div className={classes.zoom_controls}> + <StyledZoomControlsWrapper> <MiradorMenuButton aria-label={t('zoomIn')} onClick={this.handleZoomInClick}> <AddCircleIcon /> </MiradorMenuButton> @@ -66,16 +67,12 @@ export class ZoomControls extends Component { <MiradorMenuButton aria-label={t('zoomReset')} onClick={() => zoomToWorld(false)}> <RestoreZoomIcon /> </MiradorMenuButton> - {displayDivider && <span className={classes.divider} />} - </div> + </StyledZoomControlsWrapper> ); } } ZoomControls.propTypes = { - classes: PropTypes.objectOf(PropTypes.string).isRequired, - displayDivider: PropTypes.bool, - showZoomControls: PropTypes.bool, t: PropTypes.func, updateViewport: PropTypes.func, viewer: PropTypes.shape({ @@ -88,8 +85,6 @@ ZoomControls.propTypes = { }; ZoomControls.defaultProps = { - displayDivider: true, - showZoomControls: false, t: key => key, updateViewport: () => {}, viewer: {}, diff --git a/src/components/icons/BookViewIcon.js b/src/components/icons/BookViewIcon.js index a6668b1f30dd3f2334232b6a78defa451bf7e982..d26b9fe3ccb9071786167e894586d2f44d406e24 100644 --- a/src/components/icons/BookViewIcon.js +++ b/src/components/icons/BookViewIcon.js @@ -1,4 +1,4 @@ -import SvgIcon from '@material-ui/core/SvgIcon'; +import SvgIcon from '@mui/material/SvgIcon'; /** * BookViewIcon ~ diff --git a/src/components/icons/CanvasIndexIcon.js b/src/components/icons/CanvasIndexIcon.js index 8d5af98b10ffc6d5a8b644a75b88260ccf3ef5af..2bac4808d03e9a90c20f4dd4531c8a5aa99cfb90 100644 --- a/src/components/icons/CanvasIndexIcon.js +++ b/src/components/icons/CanvasIndexIcon.js @@ -1,4 +1,4 @@ -import SvgIcon from '@material-ui/core/SvgIcon'; +import SvgIcon from '@mui/material/SvgIcon'; /** * Render the canvas index svg diff --git a/src/components/icons/GalleryViewIcon.js b/src/components/icons/GalleryViewIcon.js index 9a410033404f1fb35a6260eae3892bb9f91f8d11..cccd9d2c711c176af48926783c844e35d6a35600 100644 --- a/src/components/icons/GalleryViewIcon.js +++ b/src/components/icons/GalleryViewIcon.js @@ -1,4 +1,4 @@ -import SvgIcon from '@material-ui/core/SvgIcon'; +import SvgIcon from '@mui/material/SvgIcon'; /** * GalleryViewIcon ~ diff --git a/src/components/icons/MiradorIcon.js b/src/components/icons/MiradorIcon.js index 920256dd9bf3c856729973c4343eff41265b0d60..43301fe4cb9c8bdf808da08666d037a97db9ee0b 100644 --- a/src/components/icons/MiradorIcon.js +++ b/src/components/icons/MiradorIcon.js @@ -1,4 +1,4 @@ -import SvgIcon from '@material-ui/core/SvgIcon'; +import SvgIcon from '@mui/material/SvgIcon'; /** * ThumbnailNavigationRightIcon ~ diff --git a/src/components/icons/RestoreZoomIcon.js b/src/components/icons/RestoreZoomIcon.js index 2005e6e89c5b61f851bcf398014e26581008eb7e..f1cf1e2d1e3eafc22b8eb6777115b7ea6ee0b898 100644 --- a/src/components/icons/RestoreZoomIcon.js +++ b/src/components/icons/RestoreZoomIcon.js @@ -1,4 +1,4 @@ -import SvgIcon from '@material-ui/core/SvgIcon'; +import SvgIcon from '@mui/material/SvgIcon'; /** * RestoreZoomIcon ~ diff --git a/src/components/icons/ThumbnailNavigationBottomIcon.js b/src/components/icons/ThumbnailNavigationBottomIcon.js index 5c7080a787b68f4431e3f5b31b73d9996a859eb3..4ef2b95448cac052e7cd245fe94e6fcf4fcd7e36 100644 --- a/src/components/icons/ThumbnailNavigationBottomIcon.js +++ b/src/components/icons/ThumbnailNavigationBottomIcon.js @@ -1,4 +1,4 @@ -import SvgIcon from '@material-ui/core/SvgIcon'; +import SvgIcon from '@mui/material/SvgIcon'; /** * ThumbnailNavigationBottomIcon ~ diff --git a/src/components/icons/ThumbnailNavigationRightIcon.js b/src/components/icons/ThumbnailNavigationRightIcon.js index 8f22b31d00ac81fffb763d4fc82e49c2eedc1ae6..6690b01d280f0e7b0362ca138f9cda8d4fbaa875 100644 --- a/src/components/icons/ThumbnailNavigationRightIcon.js +++ b/src/components/icons/ThumbnailNavigationRightIcon.js @@ -1,4 +1,4 @@ -import SvgIcon from '@material-ui/core/SvgIcon'; +import SvgIcon from '@mui/material/SvgIcon'; /** * ThumbnailNavigationRightIcon ~ diff --git a/src/components/icons/WindowMaxIcon.js b/src/components/icons/WindowMaxIcon.js index 9590bbb180d5ac1bbb1b322dfadc7960604efb70..3de34b6bf5e3d9efffcf7eefe179c5b229157546 100644 --- a/src/components/icons/WindowMaxIcon.js +++ b/src/components/icons/WindowMaxIcon.js @@ -1,4 +1,4 @@ -import SvgIcon from '@material-ui/core/SvgIcon'; +import SvgIcon from '@mui/material/SvgIcon'; /** * WindowMaxIcon ~ diff --git a/src/components/icons/WindowMinIcon.js b/src/components/icons/WindowMinIcon.js index 7a2ef3be9ca1ec7ffd926f0b5edad8f8b66b247c..3118b915f7e0fcd8f0a22fc1269e8bd9fa62f3ca 100644 --- a/src/components/icons/WindowMinIcon.js +++ b/src/components/icons/WindowMinIcon.js @@ -1,4 +1,4 @@ -import SvgIcon from '@material-ui/core/SvgIcon'; +import SvgIcon from '@mui/material/SvgIcon'; /** * WindowMinIcon ~ diff --git a/src/components/icons/WindowOptionsIcon.js b/src/components/icons/WindowOptionsIcon.js index 3e365c29307ee2d02b5970e4a4253f84caa5f657..e87dd282539643d1c8049c9b2a44dd4ef37913c0 100644 --- a/src/components/icons/WindowOptionsIcon.js +++ b/src/components/icons/WindowOptionsIcon.js @@ -1,4 +1,4 @@ -import SvgIcon from '@material-ui/core/SvgIcon'; +import SvgIcon from '@mui/material/SvgIcon'; /** * WindowMinIcon ~ diff --git a/src/components/icons/WorkspaceTypeElasticIcon.js b/src/components/icons/WorkspaceTypeElasticIcon.js index c817330897909d6a7daafa8fbb88d29074187a48..e1ee53918ad5afed6ff58cad42078605514b34e9 100644 --- a/src/components/icons/WorkspaceTypeElasticIcon.js +++ b/src/components/icons/WorkspaceTypeElasticIcon.js @@ -1,4 +1,4 @@ -import SvgIcon from '@material-ui/core/SvgIcon'; +import SvgIcon from '@mui/material/SvgIcon'; /** * ElasticWorkspaceIcon ~ diff --git a/src/components/icons/WorkspaceTypeMosaicIcon.js b/src/components/icons/WorkspaceTypeMosaicIcon.js index 5cdb0c34a10bed5204c03410330c7439bf0894ee..c1724ee7e88ed1b7dbf86f150f5935dcd13cf648 100644 --- a/src/components/icons/WorkspaceTypeMosaicIcon.js +++ b/src/components/icons/WorkspaceTypeMosaicIcon.js @@ -1,4 +1,4 @@ -import SvgIcon from '@material-ui/core/SvgIcon'; +import SvgIcon from '@mui/material/SvgIcon'; /** * WorkspaceTypeMosaicIcon ~ diff --git a/src/config/settings.js b/src/config/settings.js index 215801afa0782b32d8866db7620e6a50b6c6f8cb..875611bd1114564a7e972126aa79cfac00f30320 100644 --- a/src/config/settings.js +++ b/src/config/settings.js @@ -12,7 +12,7 @@ export default { themes: { dark: { palette: { - type: 'dark', + mode: 'dark', primary: { main: '#4db6ac', }, @@ -28,13 +28,13 @@ export default { }, light: { palette: { - type: 'light', + mode: 'light', } } }, theme: { // Sets up a MaterialUI theme. See https://material-ui.com/customization/default-theme/ palette: { - type: 'light', + mode: 'light', primary: { main: '#1967d2', // Controls the color of the Add button and current window indicator }, @@ -61,6 +61,7 @@ export default { }, section_divider: 'rgba(0, 0, 0, 0.25)', annotations: { + chipBackground: '#e0e0e0', hidden: { globalAlpha: 0 }, default: { strokeStyle: '#00BFFF', globalAlpha: 1 }, hovered: { strokeStyle: '#BF00FF', globalAlpha: 1 }, @@ -167,51 +168,248 @@ export default { }, useNextVariants: true // set so that console deprecation warning is removed }, - overrides: { - MuiListSubheader: { - root: { - '&[role="presentation"]:focus': { - outline: 0, + components: { + MuiMenuItem: { + variants: [ + { + props: { variant: 'multiline' }, + style: { whiteSpace: 'normal' } }, - }, + ] }, - MuiTooltip: { // Overridden from https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Tooltip/Tooltip.js#L40-L70 - tooltipPlacementLeft: { - ['@media (min-width:600px)']: { - margin: 0, + CompanionWindow: { + styleOverrides: { + closeButton: { + order: 4, }, - }, - tooltipPlacementRight: { - ['@media (min-width:600px)']: { - margin: 0, + contents: { + overflowY: 'auto', + wordBreak: 'break-word', }, - }, - tooltipPlacementTop: { - ['@media (min-width:600px)']: { - margin: 0, + controls: ({ ownerState }) => ({ + alignItems: 'center', + display: 'flex', + flexFlow: 'row wrap', + flexGrow: 1, + justifyContent: (ownerState?.position === 'bottom' || ownerState?.position === 'far-bottom') ? 'flex-end' : 'flex-start', + minHeight: 48, + order: 3 + }), + positionButton: { + marginLeft: -16, + order: -100, + width: 24, }, + resize: ({ ownerState }) => ({ + display: 'flex', + flexDirection: 'column', + minHeight: 50, + minWidth: (ownerState?.position === 'left') ? 235 : 100, + position: 'relative', + }), + root: ({ ownerState }) => ({ + boxShadow: 'none', + boxSizing: 'border-box', + display: 'flex', + flexDirection: 'column', + minHeight: 0, + ...(ownerState?.position === 'right' && { + borderLeft: '0.5px solid rgba(0, 0, 0, 0.125)' + }), + ...(ownerState?.position === 'left' && { + borderRight: '0.5px solid rgba(0, 0, 0, 0.125)' + }), + ...(ownerState?.position === 'bottom' && { + borderTop: '0.5px solid rgba(0, 0, 0, 0.125)' + }), + }), + title: ({ theme }) => ({ + ...theme.typography.subtitle1, + alignSelf: 'center', + flexGrow: 1, + width: 160 + }), + toolbar: ({ theme }) => ({ + alignItems: 'flex-start', + backgroundColor: theme.palette.shades.light, + flexWrap: 'wrap', + justifyContent: 'space-between', + minHeight: 'max-content', + paddingInlineStart: '1rem', + }), }, - tooltipPlacementBottom: { - ['@media (min-width:600px)']: { - margin: 0, + }, + CompanionWindowSection: { + styleOverrides: { + root: { + borderBlockEnd: '.5px solid rgba(0, 0, 0, 0.25)' }, }, }, - MuiTouchRipple: { - childPulsate: { - animation: 'none', + IIIFHtmlContent: { + styleOverrides: { + root: ({ theme }) => ({ + '& a': { + color: theme.palette.primary.main, + textDecoration: 'underline', + }, + }), }, - rippleVisible: { - animation: 'none', + }, + IIIFThumbnail: { + styleOverrides: { + root: ({ ownerState }) => ({ + ...(ownerState?.variant === 'inside' && { + display: 'inline-block', + height: 'inherit', + position: 'relative', + }), + }), + label: ({ ownerState }) => ({ + overflow: 'hidden', + textOverflow: 'ellipsis', + lineHeight: '1.5em', + wordBreak: 'break-word', + ...(ownerState?.variant === 'inside' && { + color: '#ffffff', + WebkitLineClamp: 1, + whiteSpace: 'nowrap', + }), + ...(ownerState?.variant === 'outside' && { + display: '-webkit-box', + maxHeight: '3em', + MozBoxOrient: 'vertical', + WebkitLineClamp: 2, + }), + ...(ownerState?.variant === 'inside' && { + background: 'linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 70%, rgba(0,0,0,0) 100%)', + bottom: '5px', + boxSizing: 'border-box', + left: '0px', + padding: '4px', + position: 'absolute', + width: '100%', + }), + }), + image: ({ ownerState }) => ({ + ...(ownerState?.border && { + border: '1px solid rgba(0, 0, 0, 0.125)', + }), + }) + } + }, + ThemeIcon: { + styleOverrides: { + icon: ({ ownerState }) => ({ + color: (ownerState?.value === 'dark' ? '#000000' : undefined) + }), }, }, - }, - props: { + MuiAccordion: { + variants: [ + { + props: { variant: 'compact' }, + style: { + '& .MuiAccordionSummary-root': { + minHeight: 'unset', + padding: 0, + }, + '& .MuiAccordionSummary-content': { + margin: 0, + }, + '& .MuiAccordionDetails-root': { + padding: 0, + }, + }, + }, + ], + }, + MuiButton: { + styleOverrides: { + inlineText: { + lineHeight: '1.5em', + padding: 0, + textAlign: 'inherit', + textTransform: 'none', + + }, + inlineTextSecondary: ({ theme }) => ({ + color: theme.palette.secondary.main, + }), + } + }, MuiButtonBase: { - disableTouchRipple: true, + defaultProps: { + disableTouchRipple: true, + }, + }, + MuiDialog: { + variants: [ + { + props: { variant: 'contained' }, + style: { + position: 'absolute', + '& .MuiBackdrop-root': { + position: 'absolute' + } + }, + } + ] + }, + MuiFab: { + styleOverrides: { + root: { + transition: 'none', + } + }, }, MuiLink: { - underline: 'always' + defaultProps: { + underline: 'always' + }, + }, + MuiListSubheader: { + styleOverrides: { + root: { + '&[role="presentation"]:focus': { + outline: 0, + }, + }, + }, + }, + MuiTooltip: { // Overridden from https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Tooltip/Tooltip.js#L40-L70 + styleOverrides: { + tooltipPlacementLeft: { + ['@media (min-width:600px)']: { + margin: '0 !important', + }, + }, + tooltipPlacementRight: { + ['@media (min-width:600px)']: { + margin: '0 !important', + }, + }, + tooltipPlacementTop: { + ['@media (min-width:600px)']: { + margin: '0 !important', + }, + }, + tooltipPlacementBottom: { + ['@media (min-width:600px)']: { + margin: '0 !important', + }, + }, + }, + }, + MuiTouchRipple: { + styleOverrides: { + childPulsate: { + animation: 'none', + }, + rippleVisible: { + animation: 'none', + }, + }, }, }, }, @@ -220,6 +418,7 @@ export default { ar: 'العربية', de: 'Deutsch', en: 'English', + fa: 'فارسی', fr: 'Français', ja: '日本語', kr: '한국어', diff --git a/src/containers/AppProviders.js b/src/containers/AppProviders.js index 22e8a3a06d4381880ecb305bea240a6df748ac1b..1ad1c6784dde9f7e5b583f13b2e39851bc397f40 100644 --- a/src/containers/AppProviders.js +++ b/src/containers/AppProviders.js @@ -11,7 +11,6 @@ import { AppProviders } from '../components/AppProviders'; */ const mapStateToProps = state => ( { - createGenerateClassNameOptions: getConfig(state).createGenerateClassNameOptions, language: getConfig(state).language, theme: getTheme(state), translations: getConfig(state).translations, diff --git a/src/containers/AttributionPanel.js b/src/containers/AttributionPanel.js index 8e0801444dd60e1786f4924b52bc0c3b3316fe7d..94ae930e0f78abee992b3e42f660b539f9a3696d 100644 --- a/src/containers/AttributionPanel.js +++ b/src/containers/AttributionPanel.js @@ -1,13 +1,12 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; -import { withPlugins } from '../extend/withPlugins'; import { getManifestLogo, getRequiredStatement, getRights, } from '../state/selectors'; +import { withPlugins } from '../extend/withPlugins'; import { AttributionPanel } from '../components/AttributionPanel'; /** @@ -21,29 +20,7 @@ const mapStateToProps = (state, { id, windowId }) => ({ rights: getRights(state, { windowId }), }); -/** - * - * @param theme - * @returns {label: {paddingLeft: number}}} - */ -const styles = theme => ({ - logo: { - maxWidth: '100%', - }, - placeholder: { - backgroundColor: theme.palette.grey[300], - }, - section: { - borderBottom: `.5px solid ${theme.palette.section_divider}`, - paddingBottom: theme.spacing(1), - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(1), - paddingTop: theme.spacing(2), - }, -}); - const enhance = compose( - withStyles(styles), withTranslation(), connect(mapStateToProps), withPlugins('AttributionPanel'), diff --git a/src/containers/AudioViewer.js b/src/containers/AudioViewer.js index e422c6d6b6e00458773de3c365080307590b9fd9..2bcd274096fcff1ff41ce76fd584853c964734ed 100644 --- a/src/containers/AudioViewer.js +++ b/src/containers/AudioViewer.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import { AudioViewer } from '../components/AudioViewer'; import { getConfig, getVisibleCanvasAudioResources, getVisibleCanvasCaptions } from '../state/selectors'; @@ -15,21 +14,8 @@ const mapStateToProps = (state, { windowId }) => ( } ); -/** */ -const styles = () => ({ - audio: { - width: '100%', - }, - container: { - alignItems: 'center', - display: 'flex', - width: '100%', - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, null), withPlugins('AudioViewer'), ); diff --git a/src/containers/CanvasAnnotations.js b/src/containers/CanvasAnnotations.js index b607523b4c476da0604f95a325181dcdf7c6435f..bc2b53b171f53efd55905405a3fa8b4c2531970d 100644 --- a/src/containers/CanvasAnnotations.js +++ b/src/containers/CanvasAnnotations.js @@ -1,7 +1,7 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { alpha, withStyles } from '@material-ui/core/styles'; +import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { diff --git a/src/containers/CanvasLayers.js b/src/containers/CanvasLayers.js index aba5f5c7c7703a0f989801e99022498742ca27cf..1b5baf2ffb1580d4fc5cf233bdec4e11bcb9b263 100644 --- a/src/containers/CanvasLayers.js +++ b/src/containers/CanvasLayers.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import * as actions from '../state/actions'; import { getCanvasLabel, @@ -26,81 +25,8 @@ const mapDispatchToProps = { updateLayers: actions.updateLayers, }; -/** For withStlyes */ -const styles = theme => ({ - dragging: {}, - dragHandle: { - alignItems: 'center', - borderRight: `0.5px solid ${theme.palette.divider}`, - display: 'flex', - flex: 1, - flexDirection: 'row', - marginBottom: -1 * theme.spacing(2) + 0.5, - marginRight: theme.spacing(1), - marginTop: -1 * theme.spacing(2), - maxWidth: theme.spacing(3), - width: theme.spacing(3), - }, - image: { - borderBottom: `1px solid ${theme.palette.divider}`, - }, - label: { - paddingLeft: theme.spacing(1), - }, - list: { - paddingTop: 0, - }, - listItem: { - '& $dragHandle': { - '&:hover': { - backgroundColor: theme.palette.action.hover, - }, - backgroundColor: theme.palette.shades.light, - }, - '&$dragging': { - '& $dragHandle, & $dragHandle:hover': { - backgroundColor: theme.palette.action.selected, - }, - backgroundColor: theme.palette.action.hover, - }, - alignItems: 'stretch', - borderBottom: `0.5px solid ${theme.palette.divider}`, - cursor: 'pointer', - paddingBottom: theme.spacing(2), - paddingRight: theme.spacing(1), - paddingTop: theme.spacing(2), - }, - opacityIcon: { - marginRight: theme.spacing(0.5), - }, - opacityInput: { - ...theme.typography.caption, - '&::-webkit-outer-spin-button,&::-webkit-inner-spin-button': { - '-webkit-appearance': 'none', - margin: 0, - }, - '-moz-appearance': 'textfield', - textAlign: 'right', - width: '3ch', - }, - sectionHeading: { - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(1), - paddingTop: theme.spacing(2), - }, - slider: { - marginLeft: theme.spacing(2), - marginRight: theme.spacing(2), - maxWidth: 150, - }, - thumbnail: { - minWidth: 50, - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), ); diff --git a/src/containers/ChangeThemeDialog.js b/src/containers/ChangeThemeDialog.js index ade8c4eeebe49ebeb76b720f099b1ec4be4863c6..a473f22f1b8346f7d7c7fee65d637eee463b79cb 100644 --- a/src/containers/ChangeThemeDialog.js +++ b/src/containers/ChangeThemeDialog.js @@ -1,6 +1,5 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/core'; import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; @@ -26,31 +25,8 @@ const mapStateToProps = state => ({ themeIds: getThemeIds(state), }); -/** */ -const styles = theme => ({ - dark: { - color: '#000000', - }, - dialogContent: { - padding: 0, - }, - light: { - color: '#BDBDBD', - }, - listitem: { - '&:focus': { - backgroundColor: theme.palette.action.focus, - }, - '&:hover': { - backgroundColor: theme.palette.action.hover, - }, - cursor: 'pointer', - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('ChangeThemeDialog'), ); diff --git a/src/containers/CollapsibleSection.js b/src/containers/CollapsibleSection.js index 3daa9258b1ae29be903287fca527ea8ba41a459d..9e32b49c242eff19469c3f98207e15b163544b80 100644 --- a/src/containers/CollapsibleSection.js +++ b/src/containers/CollapsibleSection.js @@ -1,24 +1,9 @@ import { compose } from 'redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { CollapsibleSection } from '../components/CollapsibleSection'; -const styles = { - button: { - padding: 0, - }, - container: { - display: 'flex', - justifyContent: 'space-between', - }, - heading: { - cursor: 'pointer', - }, -}; - const enhance = compose( withTranslation(), - withStyles(styles), ); export default enhance(CollapsibleSection); diff --git a/src/containers/CollectionDialog.js b/src/containers/CollectionDialog.js index 22210c1658156664525855c1142616563c5ce79b..02297bccbcae46e1233f62c569e812bbad4790b1 100644 --- a/src/containers/CollectionDialog.js +++ b/src/containers/CollectionDialog.js @@ -1,13 +1,13 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/core'; import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { - getContainerId, getManifest, getManifestoInstance, getSequenceBehaviors, getWindow, + getManifest, getManifestoInstance, getSequenceBehaviors, getWindow, } from '../state/selectors'; import { CollectionDialog } from '../components/CollectionDialog'; +import { withWorkspaceContext } from '../contexts/WorkspaceContext'; /** * mapDispatchToProps - used to hook up connect to action creators @@ -37,7 +37,6 @@ const mapStateToProps = (state, { windowId }) => { return { collection: collection && getManifestoInstance(state, { manifestId: collection.id }), collectionPath, - containerId: getContainerId(state), error: manifest && manifest.error, isMultipart: getSequenceBehaviors(state, { manifestId }).includes('multi-part'), manifest: manifest && getManifestoInstance(state, { manifestId }), @@ -48,44 +47,9 @@ const mapStateToProps = (state, { windowId }) => { }; }; -/** */ -const styles = theme => ({ - collectionFilter: { - padding: '16px', - paddingTop: 0, - }, - collectionItem: { - whiteSpace: 'normal', - }, - collectionMetadata: { - padding: '16px', - }, - dark: { - color: '#000000', - }, - dialog: { - position: 'absolute !important', - }, - dialogContent: { - padding: theme.spacing(1), - }, - light: { - color: theme.palette.grey[400], - }, - listitem: { - '&:focus': { - backgroundColor: theme.palette.action.focus, - }, - '&:hover': { - backgroundColor: theme.palette.action.hover, - }, - cursor: 'pointer', - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), + withWorkspaceContext, connect(mapStateToProps, mapDispatchToProps), withPlugins('CollectionDialog'), ); diff --git a/src/containers/CompanionArea.js b/src/containers/CompanionArea.js index f5bb041f7fcdf82e656a414e86cdc22ecc49de6c..f403a90849fa55e4ae43f23f98385d885eb3c8bc 100644 --- a/src/containers/CompanionArea.js +++ b/src/containers/CompanionArea.js @@ -1,6 +1,5 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/core'; import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend/withPlugins'; import { @@ -21,43 +20,8 @@ const mapDispatchToProps = ({ setCompanionAreaOpen: actions.setCompanionAreaOpen, }); -/** */ -const styles = theme => ({ - horizontal: { - flexDirection: 'column', - width: '100%', - }, - left: { - minWidth: 235, - }, - root: { - display: 'flex', - minHeight: 0, - position: 'relative', - zIndex: theme.zIndex.appBar - 2, - }, - toggle: { - backgroundColor: theme.palette.background.paper, - border: `1px solid ${theme.palette.shades.dark}`, - borderRadius: 0, - height: '48px', - left: '100%', - marginTop: '1rem', - padding: 2, - position: 'absolute', - width: '23px', - zIndex: theme.zIndex.drawer, - }, - toggleButton: { - marginBottom: 12, - marginTop: 12, - padding: 0, - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('CompanionArea'), ); diff --git a/src/containers/CompanionWindow.js b/src/containers/CompanionWindow.js index 29792f95824fe7a767551eff1f08187b6981d972..64a1806440f48be886d013f8cb4878be2b1f409d 100644 --- a/src/containers/CompanionWindow.js +++ b/src/containers/CompanionWindow.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withSize } from 'react-sizeme'; import { withPlugins } from '../extend/withPlugins'; import { withRef } from '../extend/withRef'; @@ -45,95 +44,9 @@ const mapDispatchToProps = (dispatch, { windowId, id }) => ({ ), }); -/** - * - * @param theme - * @returns {{closeButton: {top: number, position: string, right: number}, - * root: {overflowY: string, width: string}}} - */ -const styles = theme => ({ - closeButton: { - order: 4, - }, - 'companionWindow-bottom': { - borderTop: `0.5px solid ${theme.palette.divider}`, - }, - 'companionWindow-left': { - borderRight: `0.5px solid ${theme.palette.divider}`, - }, - 'companionWindow-right': { - borderLeft: `0.5px solid ${theme.palette.divider}`, - }, - companionWindowHeader: { - flexWrap: 'wrap', - }, - companionWindowTitleControls: { - flexGrow: 1, - order: 1000, - }, - companionWindowTitleControlsBottom: { - order: 'unset', - }, - content: { - overflowY: 'auto', - wordBreak: 'break-word', - }, - horizontal: { - }, - positionButton: { - marginLeft: -16, - order: -100, - width: 24, - }, - rnd: { - display: 'flex', - flexDirection: 'column', - minHeight: 0, - }, - root: { - boxShadow: 'none', - boxSizing: 'border-box', - display: 'flex', - flexDirection: 'column', - minHeight: 0, - }, - small: {}, - titleControls: { - alignItems: 'center', - display: 'flex', - flexFlow: 'row wrap', - minHeight: 48, - order: 3, - }, - toolbar: { - '&$small': { - '& $closeButton': { - order: 'unset', - }, - '& $titleControls': { - order: 'unset', - }, - }, - alignItems: 'flex-start', - background: theme.palette.shades.light, - justifyContent: 'space-between', - minHeight: 'max-content', - paddingLeft: theme.spacing(2), - }, - vertical: { - }, - windowSideBarTitle: { - ...theme.typography.subtitle1, - alignSelf: 'center', - flexGrow: 1, - width: 160, - }, -}); - const enhance = compose( withRef(), withTranslation(), - withStyles(styles), withSize(), connect(mapStateToProps, mapDispatchToProps), withPlugins('CompanionWindow'), diff --git a/src/containers/CustomPanel.js b/src/containers/CustomPanel.js index 493ad146335bc1e55955512e721ad1bd83244ed4..3f83547018ed6f2b604d0bdb4d506556662133b8 100644 --- a/src/containers/CustomPanel.js +++ b/src/containers/CustomPanel.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { CustomPanel } from '../components/CustomPanel'; @@ -11,17 +10,8 @@ import { CustomPanel } from '../components/CustomPanel'; const mapStateToProps = (state, { id, windowId }) => ({ }); -/** - * - * @param theme - * @returns {label: {paddingLeft: number}}} - */ -const styles = theme => ({ -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps), withPlugins('CustomPanel'), ); diff --git a/src/containers/ErrorContent.js b/src/containers/ErrorContent.js index 951d3e20158177d5b3a148e163da9e513b448839..85443562090cbcdbf4501e01a7d765d561c44ef8 100644 --- a/src/containers/ErrorContent.js +++ b/src/containers/ErrorContent.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { ErrorContent } from '../components/ErrorContent'; import { @@ -23,32 +22,8 @@ const mapStateToProps = (state, { companionWindowId, windowId }) => ({ showJsError: getConfig(state).window.showJsError, }); -/** - * @param theme - * @returns {{typographyBody: {flexGrow: number, fontSize: number|string}, - * windowTopBarStyle: {minHeight: number, paddingLeft: number, backgroundColor: string}}} - */ -const styles = theme => ({ - alert: { - '& $icon': { - color: theme.palette.error.main, - }, - backgroundColor: theme.palette.error.main, - color: '#fff', - fontWeight: theme.typography.fontWeightMedium, - }, - details: { - '& pre': { - height: '100px', - overflowY: 'scroll', - }, - flexDirection: 'column', - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps), withPlugins('ErrorContent'), ); diff --git a/src/containers/GalleryView.js b/src/containers/GalleryView.js index 6b916d755abb443155a84da0efcac4ccd453b082..9c54be5a4b3971d371f187fdafbb58490e461945 100644 --- a/src/containers/GalleryView.js +++ b/src/containers/GalleryView.js @@ -1,6 +1,5 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { GalleryView } from '../components/GalleryView'; import { getCanvases, getSequenceViewingDirection } from '../state/selectors'; @@ -17,24 +16,7 @@ const mapStateToProps = (state, { windowId }) => ( } ); -/** - * Styles to be passed to the withStyles HOC - */ -const styles = theme => ({ - galleryContainer: { - alignItems: 'flex-start', - display: 'flex', - flexDirection: 'row', - flexWrap: 'wrap', - overflowX: 'hidden', - overflowY: 'scroll', - padding: '50px 0 50px 20px', - width: '100%', - }, -}); - const enhance = compose( - withStyles(styles), connect(mapStateToProps), withPlugins('GalleryView'), // further HOC go here diff --git a/src/containers/GalleryViewThumbnail.js b/src/containers/GalleryViewThumbnail.js index 0eded38382f59cb6aec6a769eb080929b0683de7..c0306777ce9d9aecd58dfa13b733764dc95c5b51 100644 --- a/src/containers/GalleryViewThumbnail.js +++ b/src/containers/GalleryViewThumbnail.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import flatten from 'lodash/flatten'; -import { withStyles } from '@material-ui/core/styles'; import * as actions from '../state/actions'; import { GalleryViewThumbnail } from '../components/GalleryViewThumbnail'; import { @@ -12,62 +11,6 @@ import { getCompanionWindowsForContent, } from '../state/selectors'; -/** - * Styles to be passed to the withStyles HOC - */ -const styles = theme => ({ - annotationIcon: { - height: '1rem', - width: '1rem', - }, - annotationsChip: { - ...theme.typography.caption, - }, - avatar: { - backgroundColor: 'transparent', - }, - chips: { - opacity: 0.875, - position: 'absolute', - right: 0, - textAlign: 'right', - top: 0, - }, - galleryViewItem: { - '&$hasAnnotations': { - border: `2px solid ${theme.palette.action.selected}`, - }, - '&$selected,&$selected$hasAnnotations': { - border: `2px solid ${theme.palette.primary.main}`, - }, - '&:focus': { - outline: 'none', - }, - '&:hover': { - backgroundColor: theme.palette.action.hover, - }, - border: '2px solid transparent', - cursor: 'pointer', - display: 'inline-block', - margin: `${theme.spacing(1)}px ${theme.spacing(0.5)}px`, - maxHeight: props => props.config.height + 45, - minWidth: '60px', - overflow: 'hidden', - padding: theme.spacing(0.5), - position: 'relative', - width: 'min-content', - }, - hasAnnotations: {}, - searchChip: { - ...theme.typography.caption, - '&$selected $avatar': { - backgroundColor: theme.palette.highlights.primary, - }, - marginTop: 2, - }, - selected: {}, -}); - /** */ const mapStateToProps = (state, { canvas, windowId }) => { const currentCanvas = getCurrentCanvas(state, { windowId }); @@ -115,7 +58,6 @@ const mapDispatchToProps = (dispatch, { canvas, id, windowId }) => ({ const enhance = compose( connect(mapStateToProps, mapDispatchToProps), - withStyles(styles), // further HOC go here ); diff --git a/src/containers/IIIFThumbnail.js b/src/containers/IIIFThumbnail.js index c596b508cd92fee25b69b5e09e198011182363e7..6631a8deb3d5e296210213072e71a68cc30439c4 100644 --- a/src/containers/IIIFThumbnail.js +++ b/src/containers/IIIFThumbnail.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { getConfig, @@ -16,51 +15,7 @@ const mapStateToProps = (state) => ({ thumbnailsConfig: getConfig(state).thumbnails, }); -/** - * Styles for withStyles HOC - */ -const styles = theme => ({ - caption: { - lineHeight: '1.5em', - wordBreak: 'break-word', - }, - image: {}, - insideCaption: { - color: '#ffffff', - lineClamp: '1', - whiteSpace: 'nowrap', - }, - insideLabel: { - background: 'linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 70%, rgba(0,0,0,0) 100%)', - bottom: '5px', - boxSizing: 'border-box', - left: '0px', - padding: '4px', - position: 'absolute', - width: '100%', - }, - insideRoot: { - display: 'inline-block', - height: 'inherit', - position: 'relative', - }, - label: { - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - outsideCaption: { - boxOrient: 'vertical', - display: '-webkit-box', - lineClamp: '2', - maxHeight: '3em', - }, - outsideLabel: {}, - outsideRoot: {}, - root: {}, -}); - const enhance = compose( - withStyles(styles), withTranslation(), connect(mapStateToProps), withPlugins('IIIFThumbnail'), diff --git a/src/containers/LayersPanel.js b/src/containers/LayersPanel.js index 16a1a9b01b5cec0d1005c357debb1fd72dcf801d..2d7352f4eb2ce54a4927221a3e2aef820a258e23 100644 --- a/src/containers/LayersPanel.js +++ b/src/containers/LayersPanel.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { LayersPanel } from '../components/LayersPanel'; import { @@ -15,17 +14,8 @@ const mapStateToProps = (state, { id, windowId }) => ({ canvasIds: getVisibleCanvasIds(state, { windowId }), }); -/** - * - * @param theme - * @returns {label: {paddingLeft: number}}} - */ -const styles = theme => ({ -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps), withPlugins('LayersPanel'), ); diff --git a/src/containers/LocalePicker.js b/src/containers/LocalePicker.js index 509fef4e3648f8a7b4e4baaf5b0052e12dc76607..2edb53b784605e79a283efcb2a08838131297894 100644 --- a/src/containers/LocalePicker.js +++ b/src/containers/LocalePicker.js @@ -1,27 +1,9 @@ import { compose } from 'redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { LocalePicker } from '../components/LocalePicker'; -/** - * - * @param theme - * @returns {label: {paddingLeft: number}}} - */ -const styles = theme => ({ - select: { - '&:focus': { - backgroundColor: theme.palette.background.paper, - }, - }, - selectEmpty: { - backgroundColor: theme.palette.background.paper, - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), ); export default enhance(LocalePicker); diff --git a/src/containers/ManifestForm.js b/src/containers/ManifestForm.js index e64b68d68c74d31e07ae6c7c3627b761b83a22a7..02a32621445f22f08e4137054d6b2895bd0e69fb 100644 --- a/src/containers/ManifestForm.js +++ b/src/containers/ManifestForm.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { ManifestForm } from '../components/ManifestForm'; @@ -12,24 +11,8 @@ import { ManifestForm } from '../components/ManifestForm'; * @private */ const mapDispatchToProps = { addResource: actions.addResource }; -/** - * - * @param theme - */ -const styles = theme => ({ - buttons: { - textAlign: 'right', - [theme.breakpoints.up('sm')]: { - textAlign: 'inherit', - }, - }, - input: { - ...theme.typography.body1, - }, -}); const enhance = compose( - withStyles(styles), withTranslation(), connect(null, mapDispatchToProps), withPlugins('ManifestForm'), diff --git a/src/containers/ManifestInfo.js b/src/containers/ManifestInfo.js index 737b87cf7ab78b17ca91e41e1d5b61e31a8876cb..d3a129b08dfd22729ed7c9acc193e73470ea9b90 100644 --- a/src/containers/ManifestInfo.js +++ b/src/containers/ManifestInfo.js @@ -4,6 +4,7 @@ import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend/withPlugins'; import { getManifestDescription, + getManifestSummary, getManifestTitle, getManifestMetadata, } from '../state/selectors'; @@ -20,6 +21,9 @@ const mapStateToProps = (state, { id, manifestId, windowId }) => ({ }), manifestLabel: getManifestTitle(state, { companionWindowId: id, manifestId, windowId }), manifestMetadata: getManifestMetadata(state, { companionWindowId: id, manifestId, windowId }), + manifestSummary: getManifestSummary(state, { + companionWindowId: id, manifestId, windowId, + }), }); const enhance = compose( diff --git a/src/containers/ManifestListItem.js b/src/containers/ManifestListItem.js index 14ca6d70e32a8be40c7101789d594d22f9d59635..55f5c43e90be8bbb35d0a048eeaef46d74a7ae43 100644 --- a/src/containers/ManifestListItem.js +++ b/src/containers/ManifestListItem.js @@ -1,12 +1,11 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import { getManifest, getManifestTitle, getManifestThumbnail, getCanvases, - getManifestLogo, getManifestProvider, getWindowManifests, + getManifestLogo, getManifestProviderName, getWindowManifests, getManifestoInstance, getSequenceBehaviors, } from '../state/selectors'; import * as actions from '../state/actions'; @@ -32,7 +31,7 @@ const mapStateToProps = (state, { manifestId, provider }) => { && getSequenceBehaviors(state, { manifestId }).includes('multi-part'), manifestLogo: getManifestLogo(state, { manifestId }), provider: provider - || getManifestProvider(state, { manifestId }), + || getManifestProviderName(state, { manifestId }), ready: !!manifest.json, size, thumbnail: getManifestThumbnail(state, { manifestId }), @@ -50,51 +49,8 @@ const mapDispatchToProps = { fetchManifest: actions.fetchManifest, }; -/** - * - * @param theme - * @returns {{root: {}, label: {textAlign: string, textTransform: string}}} - */ -const styles = theme => ({ - active: {}, - buttonGrid: { - }, - label: { - textAlign: 'left', - textTransform: 'initial', - }, - logo: { - height: '2.5rem', - maxWidth: '100%', - objectFit: 'contain', - paddingRight: 8, - }, - placeholder: { - backgroundColor: theme.palette.grey[300], - }, - root: { - ...theme.mixins.gutters(), - '&$active': { - borderLeft: `4px solid ${theme.palette.primary.main}`, - }, - '&:hover,&:focus-within': { - '&$active': { - borderLeft: `4px solid ${theme.palette.primary.main}`, - }, - backgroundColor: theme.palette.action.hover, - borderLeft: `4px solid ${theme.palette.action.hover}`, - }, - borderLeft: '4px solid transparent', - }, - thumbnail: { - maxWidth: '100%', - objectFit: 'contain', - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('ManifestListItem'), ); diff --git a/src/containers/ManifestListItemError.js b/src/containers/ManifestListItemError.js index 001d98750b73b2916dded1f97fb205e82df2f433..40d9f5f14dd188eb03b0b691d6eae96cffee308f 100644 --- a/src/containers/ManifestListItemError.js +++ b/src/containers/ManifestListItemError.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { fetchManifest, removeResource } from '../state/actions'; import { ManifestListItemError } from '../components/ManifestListItemError'; @@ -12,26 +11,8 @@ const mapDispatchToProps = { onTryAgainClick: fetchManifest, }; -/** - * - * @param theme - * @returns {{manifestIdText: {wordBreak: string}, - * errorIcon: {color: string, width: string, height: string}}} - */ -const styles = theme => ({ - errorIcon: { - color: theme.palette.error.main, - height: '2rem', - width: '2rem', - }, - manifestIdText: { - wordBreak: 'break-all', - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(null, mapDispatchToProps), withPlugins('ManifestListItemError'), ); diff --git a/src/containers/ManifestRelatedLinks.js b/src/containers/ManifestRelatedLinks.js index 6bcc356778bf8d8f6388d25c2e2698462ea1a119..a3f1edf9e0a702b8b94fe9f28b8e1317493a0cb3 100644 --- a/src/containers/ManifestRelatedLinks.js +++ b/src/containers/ManifestRelatedLinks.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { getManifestHomepage, @@ -25,17 +24,7 @@ const mapStateToProps = (state, { id, windowId }) => ({ seeAlso: getManifestSeeAlso(state, { windowId }), }); -const styles = { - labelValueMetadata: { - '& dd': { - marginBottom: '.5em', - marginLeft: '0', - }, - }, -}; - const enhance = compose( - withStyles(styles), withTranslation(), connect(mapStateToProps), withPlugins('ManifestRelatedLinks'), diff --git a/src/containers/MinimalWindow.js b/src/containers/MinimalWindow.js index b0e4974bcbd8e50c8758b70a83c568291cac5f3a..d4e57cb75b41a5db524aaef50172dada9a6563d4 100644 --- a/src/containers/MinimalWindow.js +++ b/src/containers/MinimalWindow.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { MinimalWindow } from '../components/MinimalWindow'; @@ -22,42 +21,8 @@ const mapDispatchToProps = (dispatch, { windowId }) => ({ removeWindow: () => dispatch(actions.removeWindow(windowId)), }); -/** - * @param theme - * @returns {{typographyBody: {flexGrow: number, fontSize: number|string}, - * windowTopBarStyle: {minHeight: number, paddingLeft: number, backgroundColor: string}}} - */ -const styles = theme => ({ - button: { - marginLeft: 'auto', - }, - title: { - ...theme.typography.h6, - flexGrow: 1, - paddingLeft: theme.spacing(0.5), - }, - window: { - backgroundColor: theme.palette.shades.dark, - borderRadius: 0, - display: 'flex', - flexDirection: 'column', - height: '100%', - minHeight: 0, - overflow: 'hidden', - width: '100%', - }, - windowTopBarStyle: { - backgroundColor: theme.palette.shades.main, - borderTop: '2px solid transparent', - minHeight: 32, - paddingLeft: theme.spacing(0.5), - paddingRight: theme.spacing(0.5), - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('MinimalWindow'), ); diff --git a/src/containers/MiradorMenuButton.js b/src/containers/MiradorMenuButton.js index be1a40abdd864778c8f5e2a8d12efd02bbdec200..53658083b3c2c250932df5df654d4461c36aefdc 100644 --- a/src/containers/MiradorMenuButton.js +++ b/src/containers/MiradorMenuButton.js @@ -1,16 +1,10 @@ import { compose } from 'redux'; -import { connect } from 'react-redux'; import { withPlugins } from '../extend/withPlugins'; import { MiradorMenuButton } from '../components/MiradorMenuButton'; -import { getContainerId } from '../state/selectors'; - -/** */ -const mapStateToProps = state => ({ - containerId: getContainerId(state), -}); +import { withWorkspaceContext } from '../contexts/WorkspaceContext'; const enhance = compose( - connect(mapStateToProps, null), + withWorkspaceContext, withPlugins('MiradorMenuButton'), ); diff --git a/src/containers/MosaicRenderPreview.js b/src/containers/MosaicRenderPreview.js index 0103bab9c5beeba6658c2573ade4196ef5f2243b..149861f89f6eabdde49a83dd85592a80228a62e2 100644 --- a/src/containers/MosaicRenderPreview.js +++ b/src/containers/MosaicRenderPreview.js @@ -1,6 +1,5 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/core/styles'; import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend/withPlugins'; import { getManifestTitle } from '../state/selectors'; @@ -13,18 +12,7 @@ const mapStateToProps = (state, { windowId }) => ( } ); -/** - * - * @param theme - */ -const styles = theme => ({ - preview: { - ...theme.typography.h4, - }, -}); - const enhance = compose( - withStyles(styles), withTranslation(), connect(mapStateToProps, null), withPlugins('MosaicRenderPreview'), diff --git a/src/containers/OpenSeadragonViewer.js b/src/containers/OpenSeadragonViewer.js index bf5d3fef6b091af9709e994c6e6a49d2d206b1f3..adc01cf90c054e3d7eb0a28f26158bef22b89f5e 100644 --- a/src/containers/OpenSeadragonViewer.js +++ b/src/containers/OpenSeadragonViewer.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import flatten from 'lodash/flatten'; import { withPlugins } from '../extend/withPlugins'; import { OpenSeadragonViewer } from '../components/OpenSeadragonViewer'; @@ -55,15 +54,7 @@ const mapDispatchToProps = { updateViewport: actions.updateViewport, }; -const styles = { - osdContainer: { - flex: 1, - position: 'relative', - }, -}; - const enhance = compose( - withStyles(styles), withTranslation(), connect(mapStateToProps, mapDispatchToProps), withPlugins('OpenSeadragonViewer'), diff --git a/src/containers/PrimaryWindow.js b/src/containers/PrimaryWindow.js index 336894235f31a71867a2e77da22347d9099c7909..1895e8053964c70d57bdb34f8316dc7704832f63 100644 --- a/src/containers/PrimaryWindow.js +++ b/src/containers/PrimaryWindow.js @@ -1,6 +1,5 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { getManifestoInstance, getVisibleCanvasAudioResources, getVisibleCanvasVideoResources, getWindow, @@ -18,16 +17,7 @@ const mapStateToProps = (state, { windowId }) => { }; }; -const styles = { - primaryWindow: { - display: 'flex', - flex: 1, - position: 'relative', - }, -}; - const enhance = compose( - withStyles(styles), connect(mapStateToProps, null), withPlugins('PrimaryWindow'), ); diff --git a/src/containers/SanitizedHtml.js b/src/containers/SanitizedHtml.js index 932bfa0c0a650823e8c4d473d230fd749581b251..b6771d0d9e5585f2d35e7fd2ef7b7caaf468e334 100644 --- a/src/containers/SanitizedHtml.js +++ b/src/containers/SanitizedHtml.js @@ -1,16 +1,3 @@ -import { withStyles } from '@material-ui/core/styles'; import { SanitizedHtml } from '../components/SanitizedHtml'; -/** - * Styles for withStyles HOC - */ -const styles = theme => ({ - root: { - '& a': { - color: theme.palette.primary.main, - textDecoration: 'underline', - }, - }, -}); - -export default withStyles(styles)(SanitizedHtml); +export default SanitizedHtml; diff --git a/src/containers/ScrollIndicatedDialogContent.js b/src/containers/ScrollIndicatedDialogContent.js index 66ec29e1abc18cec67c6603ae5dbc848bec301fe..aad0df3d57d586b115f3664b4001dfe7f58ed383 100644 --- a/src/containers/ScrollIndicatedDialogContent.js +++ b/src/containers/ScrollIndicatedDialogContent.js @@ -1,29 +1,3 @@ -import { withStyles } from '@material-ui/core/styles'; import { ScrollIndicatedDialogContent } from '../components/ScrollIndicatedDialogContent'; -/** - * Styles for the withStyles HOC - */ -const styles = theme => ({ - shadowScrollDialog: { - /* Shadow covers */ - background: `linear-gradient(${theme.palette.background.paper} 30%, rgba(255, 255, 255, 0)), ` - + `linear-gradient(rgba(255, 255, 255, 0), ${theme.palette.background.paper} 70%) 0 100%, ` - // Shaddows - + 'radial-gradient(50% 0, farthest-side, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)), ' - + 'radial-gradient(50% 100%, farthest-side, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)) 0 100%,', - /* Shadow covers */ - background: `linear-gradient(${theme.palette.background.paper} 30%, rgba(255, 255, 255, 0)), ` // eslint-disable-line no-dupe-keys - + `linear-gradient(rgba(255, 255, 255, 0), ${theme.palette.background.paper} 70%) 0 100%, ` - // Shaddows - + 'radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)), ' - + 'radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)) 0 100%;', - - backgroundAttachment: 'local, local, scroll, scroll', - backgroundRepeat: 'no-repeat', - backgroundSize: '100% 40px, 100% 40px, 100% 14px, 100% 14px', - overflowY: 'auto', - }, -}); - -export default withStyles(styles)(ScrollIndicatedDialogContent); +export default ScrollIndicatedDialogContent; diff --git a/src/containers/SearchHit.js b/src/containers/SearchHit.js index 60abacf26dfe4560dcbdd509abc4d37ea33a4a1c..7e67df152f62bde81582c712da441141f1590ecf 100644 --- a/src/containers/SearchHit.js +++ b/src/containers/SearchHit.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { SearchHit } from '../components/SearchHit'; import * as actions from '../state/actions'; @@ -31,6 +30,7 @@ const mapStateToProps = (state, { windowId, }, ); + const annotationLabel = getResourceAnnotationLabel(state, { annotationUri: realAnnoId, companionWindowId, windowId }); const selectedCanvasIds = getVisibleCanvasIds(state, { windowId }); @@ -69,60 +69,8 @@ const mapDispatchToProps = (dispatch, { windowId }) => ({ ), }); -/** */ -const styles = theme => ({ - adjacent: {}, - focused: {}, - hitCounter: { - ...theme.typography.subtitle2, - backgroundColor: theme.palette.hitCounter.default, - height: 30, - marginRight: theme.spacing(1), - verticalAlign: 'inherit', - }, - inlineButton: { - '& span': { - lineHeight: '1.5em', - }, - margin: 0, - padding: 0, - textTransform: 'none', - }, - listItem: { - '&$adjacent': { - '& $hitCounter': { - backgroundColor: theme.palette.highlights.secondary, - }, - '&$windowSelected': { - '& $hitCounter': { - backgroundColor: theme.palette.highlights.primary, - }, - }, - }, - '&$windowSelected': { - '& $hitCounter': { - backgroundColor: theme.palette.highlights.primary, - }, - '&$focused': { - '&:hover': { - backgroundColor: 'inherit', - }, - backgroundColor: 'inherit', - }, - }, - borderBottom: `0.5px solid ${theme.palette.divider}`, - paddingRight: 8, - }, - selected: {}, - subtitle: { - marginBottom: theme.spacing(1.5), - }, - windowSelected: {}, -}); - const enhance = compose( connect(mapStateToProps, mapDispatchToProps), - withStyles(styles), withTranslation(), withPlugins('SearchHit'), ); diff --git a/src/containers/SearchPanel.js b/src/containers/SearchPanel.js index 64461b48c8f2ae69f343890eeaeae1d9ce34e208..5a5f4aa4088eb554c56345381e82ec0d54583c2a 100644 --- a/src/containers/SearchPanel.js +++ b/src/containers/SearchPanel.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import * as actions from '../state/actions'; import { withPlugins } from '../extend/withPlugins'; import { SearchPanel } from '../components/SearchPanel'; @@ -22,27 +21,8 @@ const mapDispatchToProps = (dispatch, props) => ({ removeSearch: () => dispatch(actions.removeSearch(props.windowId, props.id)), }); -/** -* Styles for withStyles HOC -*/ -const styles = theme => ({ - clearChip: { - marginLeft: theme.spacing(1), - }, - inlineButton: { - '& span': { - lineHeight: '1.5em', - }, - margin: theme.spacing(2), - padding: 0, - textAlign: 'inherit', - textTransform: 'none', - }, -}); - const enhance = compose( connect(mapStateToProps, mapDispatchToProps), - withStyles(styles), withTranslation(), withPlugins('SearchPanel'), ); diff --git a/src/containers/SearchPanelControls.js b/src/containers/SearchPanelControls.js index e81d46a4f3b48dedb66f93f4e2bc3034aeb77f12..e2f718ae07e613b059bc5d4ad2e206586be32b3c 100644 --- a/src/containers/SearchPanelControls.js +++ b/src/containers/SearchPanelControls.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { SearchPanelControls } from '../components/SearchPanelControls'; import * as actions from '../state/actions'; @@ -33,26 +32,8 @@ const mapDispatchToProps = { fetchSearch: actions.fetchSearch, }; -/** */ -const styles = theme => ({ - endAdornment: { - position: 'absolute', - right: 0, - }, - form: { - paddingBottom: theme.spacing(1), - paddingRight: theme.spacing(1.5), - width: '100%', - }, - searchProgress: { - position: 'absolute', - right: 0, - }, -}); - const enhance = compose( connect(mapStateToProps, mapDispatchToProps), - withStyles(styles), withTranslation(), withPlugins('SearchPanelControls'), ); diff --git a/src/containers/SearchPanelNavigation.js b/src/containers/SearchPanelNavigation.js index 614b2419209114973c256fc4029944724185b715..83b421ac3bcca2e24ca0e593390ecfca4eb56869 100644 --- a/src/containers/SearchPanelNavigation.js +++ b/src/containers/SearchPanelNavigation.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { SearchPanelNavigation } from '../components/SearchPanelNavigation'; import * as actions from '../state/actions'; @@ -37,17 +36,8 @@ const mapDispatchToProps = (dispatch, { windowId }) => ({ ), }); -/** */ -const styles = theme => ({ - body2: { - marginLeft: '-16px', - width: '100%', - }, -}); - const enhance = compose( connect(mapStateToProps, mapDispatchToProps), - withStyles(styles), withTranslation(), withPlugins('SearchPanelNavigation'), ); diff --git a/src/containers/SearchResults.js b/src/containers/SearchResults.js index 15e660d99a297bfdb9a13a57d4f19e48697c8097..0684e36b8ef5ea04d37945200b31e4484639c9fe 100644 --- a/src/containers/SearchResults.js +++ b/src/containers/SearchResults.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { SearchResults } from '../components/SearchResults'; import * as actions from '../state/actions'; @@ -33,26 +32,8 @@ const mapDispatchToProps = { fetchSearch: actions.fetchSearch, }; -/** */ -const styles = theme => ({ - moreButton: { - width: '100%', - }, - navigation: { - textTransform: 'none', - }, - noResults: { - ...theme.typography.h6, - padding: theme.spacing(2), - }, - toggleFocus: { - ...theme.typography.subtitle1, - }, -}); - const enhance = compose( connect(mapStateToProps, mapDispatchToProps), - withStyles(styles), withTranslation(), withPlugins('SearchResults'), ); diff --git a/src/containers/SelectCollection.js b/src/containers/SelectCollection.js index 446c32711f233e68c9ca4fe904e643c4e71d0ab2..e711f53708a42c3f8c395178f2c0dbca35b88e8a 100644 --- a/src/containers/SelectCollection.js +++ b/src/containers/SelectCollection.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import * as actions from '../state/actions'; import { withPlugins } from '../extend/withPlugins'; import { @@ -22,13 +21,9 @@ const mapStateToProps = (state, { windowId }) => { const mapDispatchToProps = { showCollectionDialog: actions.showCollectionDialog, }; -/** */ -const styles = (theme) => ({ -}); const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('SelectCollection'), ); diff --git a/src/containers/SidebarIndexItem.js b/src/containers/SidebarIndexItem.js index e58afff81c4fdd32cd41d211f51aff84590fed28..bc8a19001ea4d2ac41e743ecb4f338d9873506f7 100644 --- a/src/containers/SidebarIndexItem.js +++ b/src/containers/SidebarIndexItem.js @@ -1,21 +1,10 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { SidebarIndexItem } from '../components/SidebarIndexItem'; -/** - * Styles for withStyles HOC - */ -const styles = theme => ({ - label: { - paddingLeft: theme.spacing(1), - }, -}); - const enhance = compose( - withStyles(styles), withTranslation(), connect(null, null), withPlugins('SidebarIndexItem'), diff --git a/src/containers/SidebarIndexList.js b/src/containers/SidebarIndexList.js index 866e7d61d7229e239182378c8cd3e075a5a85cfa..de46a8893914f51188a78541e0899a866524b0f5 100644 --- a/src/containers/SidebarIndexList.js +++ b/src/containers/SidebarIndexList.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { @@ -29,21 +28,7 @@ const mapDispatchToProps = (dispatch, { id, windowId }) => ({ setCanvas: (...args) => dispatch(actions.setCanvas(...args)), }); -/** - * Styles for withStyles HOC - */ -const styles = theme => ({ - label: { - paddingLeft: theme.spacing(1), - }, - listItem: { - borderBottom: `0.5px solid ${theme.palette.divider}`, - paddingRight: theme.spacing(1), - }, -}); - const enhance = compose( - withStyles(styles), withTranslation(), connect(mapStateToProps, mapDispatchToProps), withPlugins('SidebarIndexList'), diff --git a/src/containers/SidebarIndexTableOfContents.js b/src/containers/SidebarIndexTableOfContents.js index 98caa554c90d31bf077dc7169a1b209a0bd28fbe..870f0c4087c0b5e0ae1ba55e59b5e048a3cc58cc 100644 --- a/src/containers/SidebarIndexTableOfContents.js +++ b/src/containers/SidebarIndexTableOfContents.js @@ -1,8 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; -import { alpha } from '@material-ui/core/styles/colorManipulator'; import { withPlugins } from '../extend/withPlugins'; import { SidebarIndexTableOfContents } from '../components/SidebarIndexTableOfContents'; import { @@ -29,49 +27,12 @@ const mapStateToProps = (state, { id, windowId }) => ({ * @private */ const mapDispatchToProps = (dispatch, { id, windowId }) => ({ + expandNodes: nodeIds => dispatch(actions.expandNodes(windowId, id, nodeIds)), setCanvas: (...args) => dispatch(actions.setCanvas(...args)), toggleNode: nodeId => dispatch(actions.toggleNode(windowId, id, nodeId)), }); -/** - * Styles for withStyles HOC - */ -const styles = theme => ({ - content: { - alignItems: 'flex-start', - borderLeft: '1px solid transparent', - padding: '8px 16px 8px 0', - width: 'auto', - }, - group: { - borderLeft: `1px solid ${theme.palette.grey[300]}`, - }, - label: { - paddingLeft: 0, - }, // needed for pseudo $label class - root: { - flexGrow: 1, - }, - selected: {}, // needed for pseudo $selected class - treeItemRoot: { - '&:focus > $content': { - backgroundColor: theme.palette.action.selected, - }, - '&:hover > $content': { - backgroundColor: theme.palette.action.hover, - }, - '&:hover >$content $label, &:focus > $content $label, &$selected > $content $label, &$selected > $content $label:hover, &$selected:focus > $content $label': { - backgroundColor: 'transparent', - }, - }, - visibleNode: { - backgroundColor: alpha(theme.palette.highlights.primary, 0.35), - display: 'inline', - }, -}); - const enhance = compose( - withStyles(styles), withTranslation(), connect(mapStateToProps, mapDispatchToProps), withPlugins('SidebarIndexTableOfContents'), diff --git a/src/containers/SidebarIndexThumbnail.js b/src/containers/SidebarIndexThumbnail.js index 9f582dc4647693cfb9d78d574648bd4169f066d1..74c831814fefcc36ec6e69a06643bce378d7ec89 100644 --- a/src/containers/SidebarIndexThumbnail.js +++ b/src/containers/SidebarIndexThumbnail.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { SidebarIndexThumbnail } from '../components/SidebarIndexThumbnail'; import { getConfig } from '../state/selectors'; @@ -15,17 +14,7 @@ const mapStateToProps = (state, { data }) => ({ ...(getConfig(state).canvasNavigation || {}), }); -/** - * Styles for withStyles HOC - */ -const styles = theme => ({ - label: { - paddingLeft: theme.spacing(1), - }, -}); - const enhance = compose( - withStyles(styles), withTranslation(), connect(mapStateToProps, null), withPlugins('SidebarIndexThumbnail'), diff --git a/src/containers/ThumbnailCanvasGrouping.js b/src/containers/ThumbnailCanvasGrouping.js index 0804cee2d51e20fbf78c28f64f4d73cef0b9aa93..548c941ba941931b77e7915ca1549a43269c5211 100644 --- a/src/containers/ThumbnailCanvasGrouping.js +++ b/src/containers/ThumbnailCanvasGrouping.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { getCurrentCanvas } from '../state/selectors'; @@ -25,33 +24,8 @@ const mapStateToProps = (state, { data }) => ({ currentCanvasId: (getCurrentCanvas(state, { windowId: data.windowId }) || {}).id, }); -/** - * Styles for withStyles HOC - */ -const styles = theme => ({ - canvas: { - '&$currentCanvas': { - outline: `2px solid ${theme.palette.primary.main}`, - outlineOffset: '3px', - }, - '&:hover': { - outline: `9px solid ${theme.palette.action.hover}`, - outlineOffset: '-2px', - }, - boxSizing: 'border-box', - color: theme.palette.common.white, - cursor: 'pointer', - display: 'inline-block', - outline: 0, - whiteSpace: 'nowrap', - }, - currentCanvas: { - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('ThumbnailCanvasGrouping'), ); diff --git a/src/containers/ThumbnailNavigation.js b/src/containers/ThumbnailNavigation.js index e3be9a8f3363f1068771976c0d71929445d9e771..bf7c3490aaa6db191facdaad429aa0c14c5087b4 100644 --- a/src/containers/ThumbnailNavigation.js +++ b/src/containers/ThumbnailNavigation.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { ThumbnailNavigation } from '../components/ThumbnailNavigation'; @@ -40,20 +39,7 @@ const mapDispatchToProps = (dispatch, { windowId }) => ({ setPreviousCanvas: (...args) => dispatch(actions.setPreviousCanvas(windowId)), }); -/** - * Styles for withStyles HOC - */ -const styles = theme => ({ - thumbNavigation: { - '&:focus': { - boxShadow: 0, - outline: 0, - }, - }, -}); - const enhance = compose( - withStyles(styles), withTranslation(), connect(mapStateToProps, mapDispatchToProps), withPlugins('ThumbnailNavigation'), diff --git a/src/containers/VideoViewer.js b/src/containers/VideoViewer.js index 824986006ca0480568e46207f564d18cf87d1c8a..11bcebb5d7a1b294d090447a6e099801b5d59ed4 100644 --- a/src/containers/VideoViewer.js +++ b/src/containers/VideoViewer.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { VideoViewer } from '../components/VideoViewer'; @@ -36,7 +35,8 @@ const mapDispatchToProps = (dispatch, { windowId }) => ({ }); /** */ -const styles = () => ({ +// TODO MergeVideo +/* const styles = () => ({ flexContainer: { alignItems: 'center', display: 'flex', @@ -51,11 +51,10 @@ const styles = () => ({ height: 'auto', width: '100%', }, -}); +}); */ const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('VideoViewer'), ); diff --git a/src/containers/ViewerInfo.js b/src/containers/ViewerInfo.js index 7e2714d9b7880b574dea33859bb272bd98445394..6757102f98ea7cf276f753ea852eb133e2938148 100644 --- a/src/containers/ViewerInfo.js +++ b/src/containers/ViewerInfo.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { ViewerInfo } from '../components/ViewerInfo'; import { @@ -29,20 +28,7 @@ const mapStateToProps = (state, props) => { }; }; -const styles = { - osdInfo: { - order: 2, - overflow: 'hidden', - paddingBottom: 3, - textOverflow: 'ellipsis', - unicodeBidi: 'plaintext', - whiteSpace: 'nowrap', - width: '100%', - }, -}; - const enhance = compose( - withStyles(styles), withTranslation(), connect(mapStateToProps, null), withPlugins('ViewerInfo'), diff --git a/src/containers/ViewerNavigation.js b/src/containers/ViewerNavigation.js index 38fb8d4602b0e24e8095486f1c13aa6edaac7a33..3f142de4315e01ad9ad214fc58e0bfdcfc58111f 100644 --- a/src/containers/ViewerNavigation.js +++ b/src/containers/ViewerNavigation.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { @@ -28,14 +27,7 @@ const mapDispatchToProps = (dispatch, { windowId }) => ({ setPreviousCanvas: (...args) => dispatch(actions.setPreviousCanvas(windowId)), }); -const styles = { - osdNavigation: { - order: 1, - }, -}; - const enhance = compose( - withStyles(styles), withTranslation(), connect(mapStateToProps, mapDispatchToProps), withPlugins('ViewerNavigation'), diff --git a/src/containers/Window.js b/src/containers/Window.js index 6e4eb5d3ea453fa54b443a224f9736b5c6b39c84..36cd4e14e1a5a87a6fd43cf8ec78a0f16e15c1f5 100644 --- a/src/containers/Window.js +++ b/src/containers/Window.js @@ -1,6 +1,5 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/core'; import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; @@ -23,7 +22,6 @@ const mapStateToProps = (state, { windowId }) => ({ sideBarOpen: (getWindow(state, { windowId }) || {}).sideBarOpen, thumbnailNavigationPosition: getThumbnailNavigationPosition(state, { windowId }), view: getWindowViewType(state, { windowId }), - window: getWindow(state, { windowId }), windowDraggable: getWindowDraggability(state, { windowId }), workspaceType: getWorkspaceType(state), }); @@ -37,70 +35,8 @@ const mapDispatchToProps = (dispatch, { windowId }) => ({ focusWindow: () => dispatch(actions.focusWindow(windowId)), }); -/** - * @param theme - */ -const styles = theme => ({ - companionAreaBottom: { - display: 'flex', - flex: '0', - flexBasis: 'auto', - minHeight: 0, - }, - companionAreaRight: { - display: 'flex', - flex: '0 1 auto', - minHeight: 0, - }, - maximized: {}, - middle: { - display: 'flex', - flex: '1', - flexDirection: 'row', - minHeight: 0, - }, - middleLeft: { - display: 'flex', - flex: '1', - flexDirection: 'column', - minHeight: 0, - }, - primaryWindow: { - display: 'flex', - flex: '1', - height: '300px', - minHeight: 0, - position: 'relative', - }, - thumbnailArea: { - backgroundColor: theme.palette.shades.dark, - }, - thumbnailAreaBottom: { - }, - thumbnailAreaRight: { - minWidth: 100, - }, - window: { - '&$maximized': { - left: 0, - position: 'absolute', - top: 0, - zIndex: theme.zIndex.modal - 1, - }, - backgroundColor: theme.palette.shades.dark, - borderRadius: 0, - display: 'flex', - flexDirection: 'column', - height: '100%', - minHeight: 0, - overflow: 'hidden', - width: '100%', - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('Window'), ); diff --git a/src/containers/WindowAuthenticationBar.js b/src/containers/WindowAuthenticationBar.js index f3b7f8733367a8b7f2adf4c1e35f74d49b3353f1..b38a244aa80c6a2c31568c07caff1133affde8ab 100644 --- a/src/containers/WindowAuthenticationBar.js +++ b/src/containers/WindowAuthenticationBar.js @@ -1,62 +1,10 @@ import { compose } from 'redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; -import { alpha } from '@material-ui/core/styles/colorManipulator'; import { withPlugins } from '../extend/withPlugins'; import { WindowAuthenticationBar } from '../components/WindowAuthenticationBar'; -/** - * @param theme - * @returns {{typographyBody: {flexGrow: number, fontSize: number|string}, - * windowTopBarStyle: {minHeight: number, paddingLeft: number, backgroundColor: string}}} - */ -const styles = theme => ({ - buttonInvert: { - '&:hover': { - backgroundColor: alpha(theme.palette.secondary.contrastText, 1 - theme.palette.action.hoverOpacity), - }, - backgroundColor: theme.palette.secondary.contrastText, - marginLeft: theme.spacing(5), - paddingBottom: 0, - paddingTop: 0, - }, - expanded: { - paddingLeft: theme.spacing(), - paddingRight: theme.spacing(), - }, - failure: { - backgroundColor: theme.palette.error.dark, - }, - fauxButton: { - marginLeft: theme.spacing(2.5), - }, - icon: { - marginRight: theme.spacing(1.5), - verticalAlign: 'text-bottom', - }, - label: { - lineHeight: 2.25, - }, - paper: { - backgroundColor: theme.palette.secondary.main, - color: theme.palette.secondary.contrastText, - cursor: 'pointer', - }, - topBar: { - '&:hover': { - backgroundColor: theme.palette.secondary.main, - }, - alignItems: 'center', - display: 'flex', - justifyContent: 'inherit', - padding: theme.spacing(1), - textTransform: 'none', - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), withPlugins('WindowAuthenticationBar'), ); diff --git a/src/containers/WindowCanvasNavigationControls.js b/src/containers/WindowCanvasNavigationControls.js index 8028c13df02bf98720c722060c945e7cd2042281..ca5088f34b6e32c34ef511435f01dd01143add70 100644 --- a/src/containers/WindowCanvasNavigationControls.js +++ b/src/containers/WindowCanvasNavigationControls.js @@ -1,44 +1,18 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { withSize } from 'react-sizeme'; -import { withStyles } from '@material-ui/core'; -import { alpha } from '@material-ui/core/styles/colorManipulator'; import { withPlugins } from '../extend/withPlugins'; -import { getWorkspace } from '../state/selectors'; +import { getShowZoomControlsConfig, getWorkspace } from '../state/selectors'; import { WindowCanvasNavigationControls } from '../components/WindowCanvasNavigationControls'; /** */ const mapStateToProps = (state, { windowId }) => ({ + showZoomControls: getShowZoomControlsConfig(state), visible: getWorkspace(state).focusedWindowId === windowId, }); -/** - * - * @param theme - */ -const styles = theme => ({ - canvasNav: { - display: 'flex', - flexDirection: 'row', - flexWrap: 'wrap', - justifyContent: 'center', - textAlign: 'center', - }, - canvasNavStacked: { - flexDirection: 'column', - }, - controls: { - backgroundColor: alpha(theme.palette.background.paper, 0.5), - bottom: 0, - position: 'absolute', - width: '100%', - zIndex: 50, - }, -}); - const enhance = compose( connect(mapStateToProps), - withStyles(styles), withSize(), withPlugins('WindowCanvasNavigationControls'), ); diff --git a/src/containers/WindowList.js b/src/containers/WindowList.js index 0d8f32c4d4d0394eca87db97c00ca0528a626384..3ceaec34d16ff5a5cc469bd30490d1a1993c3b7f 100644 --- a/src/containers/WindowList.js +++ b/src/containers/WindowList.js @@ -3,8 +3,9 @@ import { compose } from 'redux'; import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; -import { getContainerId, getWindowIds, getWindowTitles } from '../state/selectors'; +import { getFocusedWindowId, getWindowIds, getWindowTitles } from '../state/selectors'; import { WindowList } from '../components/WindowList'; +import { withWorkspaceContext } from '../contexts/WorkspaceContext'; /** * mapDispatchToProps - used to hook up connect to action creators @@ -22,7 +23,7 @@ const mapDispatchToProps = { */ const mapStateToProps = state => ( { - containerId: getContainerId(state), + focusedWindowId: getFocusedWindowId(state), titles: getWindowTitles(state), windowIds: getWindowIds(state), } @@ -30,6 +31,7 @@ const mapStateToProps = state => ( const enhance = compose( withTranslation(), + withWorkspaceContext, connect(mapStateToProps, mapDispatchToProps), withPlugins('WindowList'), ); diff --git a/src/containers/WindowListButton.js b/src/containers/WindowListButton.js index 74506d9dbaec7e421ae6288d6eb93e64210aeaec..68e984e497f7addbed5b701fcc07ec7499cae9bf 100644 --- a/src/containers/WindowListButton.js +++ b/src/containers/WindowListButton.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { getWindowIds, getWorkspace } from '../state/selectors'; import { WindowListButton } from '../components/WindowListButton'; @@ -12,26 +11,8 @@ const mapStateToProps = (state) => ({ windowCount: getWindowIds(state).length, }); -/** - * - * @param theme - * @returns {{background: {background: string}}} - */ -const styles = theme => ({ - badge: { - paddingLeft: 12, - }, - ctrlBtn: { - margin: theme.spacing(1), - }, - ctrlBtnSelected: { - backgroundColor: theme.palette.action.selected, - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, null), withPlugins('WindowListButton'), ); diff --git a/src/containers/WindowSideBar.js b/src/containers/WindowSideBar.js index 012b2fd5d5cf273865948294150c6bb72c6d8043..022e34f310dbb64f9f64c5492c165314e84c7430 100644 --- a/src/containers/WindowSideBar.js +++ b/src/containers/WindowSideBar.js @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; -import { withStyles } from '@material-ui/core'; import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend/withPlugins'; import { WindowSideBar } from '../components/WindowSideBar'; @@ -19,32 +18,7 @@ const mapStateToProps = (state, { windowId }) => ( } ); -/** - * - * @param theme - * @returns {{toolbar: CSSProperties | toolbar | {minHeight}, grow: {flexGrow: number}, - * drawer: {overflowX: string, left: number, flexShrink: number, width: number, height: string}}} - */ -const styles = theme => ({ - drawer: { - flexShrink: 0, - height: '100%', - order: -1000, - zIndex: theme.zIndex.appBar - 1, - }, - grow: { - flexGrow: 1, - }, - paper: { - borderInlineEnd: `1px solid ${theme.palette.divider}`, - overflowX: 'hidden', - width: 48, - }, - toolbar: theme.mixins.toolbar, -}); - const enhance = compose( - withStyles(styles), withTranslation(), connect(mapStateToProps, null), withPlugins('WindowSideBar'), diff --git a/src/containers/WindowSideBarAnnotationsPanel.js b/src/containers/WindowSideBarAnnotationsPanel.js index 904cea2f93053bcbca8d2c0903931f934ccc6e9d..8370a0c375692dbbf729443fe0607ebab4de0621 100644 --- a/src/containers/WindowSideBarAnnotationsPanel.js +++ b/src/containers/WindowSideBarAnnotationsPanel.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { getVisibleCanvasIds, @@ -22,20 +21,8 @@ const mapStateToProps = (state, { windowId }) => ({ canvasIds: getVisibleCanvasIds(state, { windowId }), }); -/** */ -const styles = theme => ({ - section: { - borderBottom: `.5px solid ${theme.palette.section_divider}`, - paddingBottom: theme.spacing(1), - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(1), - paddingTop: theme.spacing(2), - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, null), withPlugins('WindowSideBarAnnotationsPanel'), // further HOC diff --git a/src/containers/WindowSideBarButtons.js b/src/containers/WindowSideBarButtons.js index 4327eec2687fdfacd6b91721de5c7af22eb8eb0c..3b0511ae0fa9c694439f68416106ef173881b1d4 100644 --- a/src/containers/WindowSideBarButtons.js +++ b/src/containers/WindowSideBarButtons.js @@ -1,6 +1,5 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/core'; import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; @@ -65,49 +64,8 @@ const mapStateToProps = (state, { windowId }) => ({ sideBarPanel: ((getCompanionWindowsForPosition(state, { position: 'left', windowId }))[0] || {}).content, }); -/** */ -const style = theme => ({ - badge: { - backgroundColor: theme.palette.notification.main, - }, - tab: { - '&:active': { - backgroundColor: theme.palette.action.active, - }, - '&:focus': { - '@media (hover: none)': { - backgroundColor: 'transparent', - }, - backgroundColor: theme.palette.action.hover, - textDecoration: 'none', - // Reset on touch devices, it doesn't add specificity - }, - '&:hover': { - '@media (hover: none)': { - backgroundColor: 'transparent', - }, - backgroundColor: theme.palette.action.hover, - textDecoration: 'none', - // Reset on touch devices, it doesn't add specificity - }, - - borderRight: '2px solid transparent', - minWidth: 'auto', - }, - tabSelected: { - borderRight: `2px solid ${theme.palette.primary.main}`, - }, - tabsFlexContainer: { - flexDirection: 'column', - }, - tabsIndicator: { - display: 'none', - }, -}); - const enhance = compose( withTranslation(), - withStyles(style), connect(mapStateToProps, mapDispatchToProps), withPlugins('WindowSideBarButtons'), ); diff --git a/src/containers/WindowSideBarCanvasPanel.js b/src/containers/WindowSideBarCanvasPanel.js index 24324d253865b996f88b608e36c67245c11a0f36..e6b4befc734f459a96201631e3d1be18cb0b94cb 100644 --- a/src/containers/WindowSideBarCanvasPanel.js +++ b/src/containers/WindowSideBarCanvasPanel.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { WindowSideBarCanvasPanel } from '../components/WindowSideBarCanvasPanel'; @@ -53,37 +52,8 @@ const mapDispatchToProps = (dispatch, { id, windowId }) => ({ ), }); -/** - * - * @param theme - */ -const styles = theme => ({ - break: { - flexBasis: '100%', - height: 0, - }, - collectionNavigationButton: { - textTransform: 'none', - }, - label: { - paddingLeft: theme.spacing(1), - }, - select: { - '&:focus': { - backgroundColor: theme.palette.background.paper, - }, - }, - selectEmpty: { - backgroundColor: theme.palette.background.paper, - }, - variantTab: { - minWidth: 'auto', - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('WindowSideBarCanvasPanel'), ); diff --git a/src/containers/WindowSideBarCollectionPanel.js b/src/containers/WindowSideBarCollectionPanel.js index 32b31a77b283f90756a568cffe44be7282f31d5b..738c11125a4d23e87226f674a85ee2423d71603c 100644 --- a/src/containers/WindowSideBarCollectionPanel.js +++ b/src/containers/WindowSideBarCollectionPanel.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { @@ -56,22 +55,7 @@ const mapDispatchToProps = (dispatch, { id, windowId }) => ({ updateWindow: (...args) => dispatch(actions.updateWindow(windowId, ...args)), }); -/** - * Styles for withStyles HOC - */ -const styles = theme => ({ - label: { - paddingLeft: theme.spacing(1), - }, - menuItem: { - borderBottom: `0.5px solid ${theme.palette.divider}`, - paddingRight: theme.spacing(1), - whiteSpace: 'normal', - }, -}); - const enhance = compose( - withStyles(styles), withTranslation(), connect(mapStateToProps, mapDispatchToProps), withPlugins('WindowSideBarCollectionPanel'), diff --git a/src/containers/WindowSideBarInfoPanel.js b/src/containers/WindowSideBarInfoPanel.js index 9fc498ce117521c64e176a2559bebd8c8b40692d..19de86776b88a5277762ce3e4b8c74eb95cf9e86 100644 --- a/src/containers/WindowSideBarInfoPanel.js +++ b/src/containers/WindowSideBarInfoPanel.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { @@ -33,24 +32,8 @@ const mapDispatchToProps = (dispatch, { windowId, id }) => ({ setLocale: locale => dispatch(actions.updateCompanionWindow(windowId, id, { locale })), }); -/** - * - * @param theme - * @returns {label: {paddingLeft: number}}} - */ -const styles = theme => ({ - section: { - borderBottom: `.5px solid ${theme.palette.section_divider}`, - paddingBottom: theme.spacing(1), - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(1), - paddingTop: theme.spacing(2), - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('WindowSideBarInfoPanel'), ); diff --git a/src/containers/WindowThumbnailSettings.js b/src/containers/WindowThumbnailSettings.js index 24878457a780b19e3e280e0e289179636d0fb28b..d773247feef8d32d57fcaa1835dba73c3eb9dff3 100644 --- a/src/containers/WindowThumbnailSettings.js +++ b/src/containers/WindowThumbnailSettings.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { getThumbnailNavigationPosition, getThemeDirection } from '../state/selectors'; @@ -26,22 +25,7 @@ const mapStateToProps = (state, { windowId }) => ( } ); -/** */ -const styles = theme => ({ - label: { - borderBottom: '2px solid transparent', - }, - MenuItem: { - display: 'inline-block', - }, - selectedLabel: { - borderBottom: `2px solid ${theme.palette.secondary.main}`, - color: theme.palette.secondary.main, - }, -}); - const enhance = compose( - withStyles(styles), withTranslation(null, { withRef: true }), connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true }), withPlugins('WindowThumbnailSettings'), diff --git a/src/containers/WindowTopBar.js b/src/containers/WindowTopBar.js index 82148be2f4cbe397f2c5d3edd1cfb0e928b10734..ae9fc34b90eef656931403890f734f980993ac6b 100644 --- a/src/containers/WindowTopBar.js +++ b/src/containers/WindowTopBar.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { getWindowConfig, isFocused } from '../state/selectors'; @@ -35,31 +34,8 @@ const mapDispatchToProps = (dispatch, { windowId }) => ({ toggleWindowSideBar: () => dispatch(actions.toggleWindowSideBar(windowId)), }); -/** - * @param theme - * @returns {{typographyBody: {flexGrow: number, fontSize: number|string}, - * windowTopBarStyle: {minHeight: number, paddingLeft: number, backgroundColor: string}}} - */ -const styles = theme => ({ - focused: {}, - windowTopBarStyle: { - '&$focused': { - borderTop: `2px solid ${theme.palette.primary.main}`, - }, - backgroundColor: theme.palette.shades.main, - borderTop: '2px solid transparent', - minHeight: 32, - paddingLeft: theme.spacing(0.5), - paddingRight: theme.spacing(0.5), - }, - windowTopBarStyleDraggable: { - cursor: 'move', - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('WindowTopBar'), ); diff --git a/src/containers/WindowTopBarPluginArea.js b/src/containers/WindowTopBarPluginArea.js index 4bcdc88f5ae95da56b721b52d856012ded348c0a..cf05b64af3e923718c58de9d28f952c2e8147d50 100644 --- a/src/containers/WindowTopBarPluginArea.js +++ b/src/containers/WindowTopBarPluginArea.js @@ -1,17 +1,11 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import { WindowTopBarPluginArea } from '../components/WindowTopBarPluginArea'; -/** - */ -const styles = {}; - const enhance = compose( withTranslation(), - withStyles(styles), connect(null, null), withPlugins('WindowTopBarPluginArea'), ); diff --git a/src/containers/WindowTopBarPluginMenu.js b/src/containers/WindowTopBarPluginMenu.js index 946d9d0b5db92798d0ab882cdda9736bca96154c..4900528e9f5cd7ca363f978ced229ed8c46bff41 100644 --- a/src/containers/WindowTopBarPluginMenu.js +++ b/src/containers/WindowTopBarPluginMenu.js @@ -1,35 +1,12 @@ import { compose } from 'redux'; -import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import { WindowTopBarPluginMenu } from '../components/WindowTopBarPluginMenu'; -import { getContainerId } from '../state/selectors'; - -/** - * mapStateToProps - to hook up connect - * @memberof WindowTopBarPluginMenu - * @private - */ -const mapStateToProps = state => ({ - containerId: getContainerId(state), -}); - -/** - * - * @param theme - * @returns {{ctrlBtn: {margin: (number|string)}}} - */ -const styles = theme => ({ - ctrlBtnSelected: { - backgroundColor: theme.palette.action.selected, - }, -}); +import { withWorkspaceContext } from '../contexts/WorkspaceContext'; const enhance = compose( withTranslation(), - withStyles(styles), - connect(mapStateToProps, null), + withWorkspaceContext, withPlugins('WindowTopBarPluginMenu'), ); diff --git a/src/containers/WindowTopBarTitle.js b/src/containers/WindowTopBarTitle.js index 5cb51b8274b7ffc7b1bca026c5b5fe5cfea0207f..fbdf31288be412e6bddec49981aa5a6a0a4838d3 100644 --- a/src/containers/WindowTopBarTitle.js +++ b/src/containers/WindowTopBarTitle.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import { getManifestStatus, getManifestTitle, getWindowConfig } from '../state/selectors'; import { WindowTopBarTitle } from '../components/WindowTopBarTitle'; @@ -14,20 +13,8 @@ const mapStateToProps = (state, { windowId }) => ({ manifestTitle: getManifestTitle(state, { windowId }), }); -/** - * @param theme - */ -const styles = theme => ({ - title: { - ...theme.typography.h6, - flexGrow: 1, - paddingLeft: theme.spacing(0.5), - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, null), withPlugins('WindowTopBarTitle'), ); diff --git a/src/containers/WindowTopMenu.js b/src/containers/WindowTopMenu.js index f042c1f732015023d7a5055be3209b2a6403c418..eb12b7bc51ed61d8e78a90be599da478d32f6ecb 100644 --- a/src/containers/WindowTopMenu.js +++ b/src/containers/WindowTopMenu.js @@ -4,7 +4,8 @@ import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { WindowTopMenu } from '../components/WindowTopMenu'; -import { getConfig, getContainerId } from '../state/selectors'; +import { getConfig } from '../state/selectors'; +import { withWorkspaceContext } from '../contexts/WorkspaceContext'; /** * mapStateToProps - to hook up connect @@ -12,7 +13,6 @@ import { getConfig, getContainerId } from '../state/selectors'; * @private */ const mapStateToProps = state => ({ - containerId: getContainerId(state), showThumbnailNavigationSettings: getConfig(state).thumbnailNavigation.displaySettings, }); @@ -27,6 +27,7 @@ const mapDispatchToProps = dispatch => ({ const enhance = compose( withTranslation(), + withWorkspaceContext, connect(mapStateToProps, mapDispatchToProps), withPlugins('WindowTopMenu'), ); diff --git a/src/containers/WindowTopMenuButton.js b/src/containers/WindowTopMenuButton.js index 54d863e2b688ca032ac5ed7af9e093e176b7fc18..8a114a8dea2d3d6f7c2b455ab27f820b8f97653d 100644 --- a/src/containers/WindowTopMenuButton.js +++ b/src/containers/WindowTopMenuButton.js @@ -1,23 +1,10 @@ import { compose } from 'redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import { WindowTopMenuButton } from '../components/WindowTopMenuButton'; -/** - * - * @param theme - * @returns {{ctrlBtn: {margin: (number|string)}}} - */ -const styles = theme => ({ - ctrlBtnSelected: { - backgroundColor: theme.palette.action.selected, - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), withPlugins('WindowTopMenuButton'), ); diff --git a/src/containers/WindowViewSettings.js b/src/containers/WindowViewSettings.js index 6fc417f284b1f5b585c85ec369ddbd31946ece8a..3e34e5949b1ede7323c50648a8699cd3f111dfe7 100644 --- a/src/containers/WindowViewSettings.js +++ b/src/containers/WindowViewSettings.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { getAllowedWindowViewTypes, getWindowViewType } from '../state/selectors'; @@ -26,22 +25,7 @@ const mapStateToProps = (state, { windowId }) => ( } ); -/** */ -const styles = theme => ({ - label: { - borderBottom: '2px solid transparent', - }, - MenuItem: { - display: 'inline-block', - }, - selectedLabel: { - borderBottom: `2px solid ${theme.palette.secondary.main}`, - color: theme.palette.secondary.main, - }, -}); - const enhance = compose( - withStyles(styles), withTranslation(null, { withRef: true }), connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true }), withPlugins('WindowViewSettings'), diff --git a/src/containers/Workspace.js b/src/containers/Workspace.js index 6eac59013b39931e67ce89e965cb58bfa3678c7b..b7a8881f87923414cebff803a0923557c40ba953 100644 --- a/src/containers/Workspace.js +++ b/src/containers/Workspace.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { Workspace } from '../components/Workspace'; import { @@ -35,35 +34,8 @@ const mapDispatchToProps = { addWindow: actions.addWindow, }; -/** - * @param theme - */ -const styles = theme => ({ - workspaceViewport: { - bottom: 0, - left: 0, - margin: 0, - overflow: 'hidden', - position: 'absolute', - right: 0, - top: 0, - }, - workspaceWithControlPanel: { - paddingTop: 74, - }, - // injection order matters here - // eslint-disable-next-line sort-keys - '@media (min-width: 600px)': { - workspaceWithControlPanel: { - paddingLeft: 68, - paddingTop: 0, - }, - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('Workspace'), // further HOC go here diff --git a/src/containers/WorkspaceAdd.js b/src/containers/WorkspaceAdd.js index fe7b6c68232c39a5a2e9a4f7e815cf198335f288..0c0883945d1b04b1e8ea07aa1e822f5486ac15b9 100644 --- a/src/containers/WorkspaceAdd.js +++ b/src/containers/WorkspaceAdd.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { WorkspaceAdd } from '../components/WorkspaceAdd'; @@ -24,68 +23,8 @@ const mapDispatchToProps = { setWorkspaceAddVisibility: actions.setWorkspaceAddVisibility, }; -/** - * - * @param theme - * @returns {{typographyBody: {flexGrow: number, fontSize: string}, - * form: {paddingBottom: number, paddingTop: number, marginTop: number}, - * fab: {bottom: number, position: string, right: number}, - * menuButton: {marginRight: number, marginLeft: number}}} - */ -const styles = theme => ({ - displayNone: { - display: 'none', - }, - fab: { - bottom: theme.spacing(2), - position: 'absolute', - right: theme.spacing(2), - }, - form: { - ...theme.mixins.gutters(), - left: '0', - marginTop: 48, - paddingBottom: theme.spacing(2), - paddingTop: theme.spacing(2), - right: '0', - }, - list: { - margin: '16px', - }, - menuButton: { - marginLeft: -12, - marginRight: 20, - }, - paper: { - borderTop: '0', - left: '0', - [theme.breakpoints.up('sm')]: { - left: '65px', - }, - }, - typographyBody: { - flexGrow: 1, - }, - workspaceAdd: { - boxSizing: 'border-box', - height: '100%', - overflowX: 'hidden', - overflowY: 'auto', - paddingTop: 68, - }, - // injection order matters - // eslint-disable-next-line sort-keys - '@media (min-width: 600px)': { - workspaceAdd: { - paddingLeft: 68, - paddingTop: 0, - }, - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('WorkspaceAdd'), ); diff --git a/src/containers/WorkspaceAddButton.js b/src/containers/WorkspaceAddButton.js index 642e83fc482c9fbcf3404c6d35fb4701e4d48c10..cec63fcb4a2e3b12701ff83936e7a44349af6e4f 100644 --- a/src/containers/WorkspaceAddButton.js +++ b/src/containers/WorkspaceAddButton.js @@ -1,13 +1,37 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; -import withWidth from '@material-ui/core/withWidth'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { getWindowIds, getWorkspace } from '../state/selectors'; import { WorkspaceAddButton } from '../components/WorkspaceAddButton'; +/** + * Be careful using this hook. It only works because the number of + * breakpoints in theme is static. It will break once you change the number of + * breakpoints. See https://legacy.reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level + */ +function useWidth() { + const theme = useTheme(); + const keys = [...theme.breakpoints.keys].reverse(); + return ( + keys.reduce((output, key) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const matches = useMediaQuery(theme.breakpoints.up(key)); + return !output && matches ? key : output; + }, null) || 'xs' + ); +} + +/** + * withWidth + * @memberof WorkspaceControlPanel + * @private + */ +const withWidth = () => (WrappedComponent) => (props) => <WrappedComponent {...props} width={useWidth()} />; + /** * mapStateToProps - to hook up connect * @memberof WorkspaceControlPanel @@ -32,31 +56,9 @@ const mapStateToProps = (state, { width }) => { */ const mapDispatchToProps = { setWorkspaceAddVisibility: actions.setWorkspaceAddVisibility }; -/** - * - * @param theme - * @returns {{ctrlBtn: {margin: (number|string)}}} - */ -const styles = theme => ({ - fab: { - margin: theme.spacing(1), - }, - fabPrimary: { - '&:focus': { - backgroundColor: theme.palette.primary.dark, - }, - }, - fabSecondary: { - '&:focus': { - backgroundColor: theme.palette.secondary.dark, - }, - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), - withWidth(), + withWidth({ initialWidth: 'xs' }), connect(mapStateToProps, mapDispatchToProps), withPlugins('WorkspaceAddButton'), ); diff --git a/src/containers/WorkspaceArea.js b/src/containers/WorkspaceArea.js index c2655978c98e9a934c485eccbf5984192b2aa31a..f58fa21317a454ecbeba92513635bb76dc634b74 100644 --- a/src/containers/WorkspaceArea.js +++ b/src/containers/WorkspaceArea.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles, lighten, darken } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { WorkspaceArea } from '../components/WorkspaceArea'; import { getConfig, getWindowIds, getWorkspace } from '../state/selectors'; @@ -20,30 +19,8 @@ const mapStateToProps = state => ( } ); -/** - * - * @param theme - * @returns {{background: {background: string}}} - */ -const styles = (theme) => { - const getBackgroundColor = theme.palette.type === 'light' ? darken : lighten; - - return { - viewer: { - background: getBackgroundColor(theme.palette.shades.light, 0.1), - bottom: 0, - left: 0, - overflow: 'hidden', - position: 'absolute', - right: 0, - top: 0, - }, - }; -}; - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps), withPlugins('WorkspaceArea'), ); diff --git a/src/containers/WorkspaceControlPanel.js b/src/containers/WorkspaceControlPanel.js index 88f482e91d3a8cb92d78770c855eae6e7bda8907..e23bb417a950d24c6d7cb7bd6ec5bd690758e92b 100644 --- a/src/containers/WorkspaceControlPanel.js +++ b/src/containers/WorkspaceControlPanel.js @@ -1,69 +1,10 @@ import { compose } from 'redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { WorkspaceControlPanel } from '../components/WorkspaceControlPanel'; -/** - * - * @param theme - * @returns {{ctrlBtn: {margin: (number|string)}, - * drawer: {overflowX: string, height: string}}} - */ -const styles = theme => ({ - branding: { - display: 'flex', - position: 'absolute', - [theme.breakpoints.up('xs')]: { - display: 'none', - }, - [theme.breakpoints.up('sm')]: { - bottom: 0, - display: 'block', - float: 'none', - right: 'auto', - width: '100%', - }, - right: 0, - }, - ctrlBtn: { - margin: theme.spacing(1), - }, - drawer: { - overflowX: 'hidden', - }, - root: { - height: 64, - [theme.breakpoints.up('sm')]: { - height: '100%', - left: 0, - right: 'auto', - width: 64, - }, - }, - toolbar: { - display: 'flex', - justifyContent: 'space-between', - [theme.breakpoints.up('sm')]: { - flexDirection: 'column', - justifyContent: 'flex-start', - minHeight: 0, - }, - }, - wide: { - width: 'auto', - }, - workspaceButtons: { - [theme.breakpoints.up('sm')]: { - display: 'flex', - flexDirection: 'column', - }, - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), withPlugins('WorkspaceControlPanel'), // further HOC go here ); diff --git a/src/containers/WorkspaceControlPanelButtons.js b/src/containers/WorkspaceControlPanelButtons.js index eaef08bd775196782632a0bb6022759aa41b3d57..9d500357b4190a08d0a7b3a1a278fc067c06beb2 100644 --- a/src/containers/WorkspaceControlPanelButtons.js +++ b/src/containers/WorkspaceControlPanelButtons.js @@ -1,21 +1,8 @@ import { compose } from 'redux'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { WorkspaceControlPanelButtons } from '../components/WorkspaceControlPanelButtons'; -/** - * - * @param theme - * @returns {{ctrlBtn: {margin: (number|string)}}} - */ -const styles = theme => ({ - ctrlBtn: { - margin: theme.spacing(1), - }, -}); - const enhance = compose( - withStyles(styles), withPlugins('WorkspaceControlPanelButtons'), ); diff --git a/src/containers/WorkspaceElastic.js b/src/containers/WorkspaceElastic.js index 9badc4614bbc259c42c3f64d6ca1654b37343fcc..5cc72f575697d02c3a32bdb5ddb7394ad1ffd505 100644 --- a/src/containers/WorkspaceElastic.js +++ b/src/containers/WorkspaceElastic.js @@ -1,6 +1,5 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import WorkspaceElastic from '../components/WorkspaceElastic'; @@ -44,22 +43,7 @@ const mapDispatchToProps = (dispatch, props) => ({ }, }); -const styles = { - workspace: { - boxSizing: 'border-box', - margin: 0, - position: 'absolute', - transitionDuration: '.7s', - // order matters - // eslint-disable-next-line sort-keys - '& .react-draggable-dragging': { - transitionDuration: 'unset', - }, - }, -}; - const enhance = compose( - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('WorkspaceElastic'), // further HOC go here diff --git a/src/containers/WorkspaceElasticWindow.js b/src/containers/WorkspaceElasticWindow.js index e619b40e388e61baa51c2565978aa8326739a08c..7e6b5ccac2ff2756781fd1a66208580554442e78 100644 --- a/src/containers/WorkspaceElasticWindow.js +++ b/src/containers/WorkspaceElasticWindow.js @@ -1,6 +1,5 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/core'; import * as actions from '../state/actions'; import WorkspaceElasticWindow from '../components/WorkspaceElasticWindow'; import { @@ -28,6 +27,7 @@ const mapStateToProps = (state, { windowId }) => ( * @private */ const mapDispatchToProps = (dispatch, props) => ({ + focusWindow: () => dispatch(actions.focusWindow(props.windowId)), updateElasticWindowLayout: (windowId, position) => { dispatch( actions.updateElasticWindowLayout(windowId, position), @@ -35,17 +35,7 @@ const mapDispatchToProps = (dispatch, props) => ({ }, }); -/** - * @param theme - */ -const styles = theme => ({ - focused: { - zIndex: theme.zIndex.modal - 1, - }, -}); - const enhance = compose( - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), // further HOC go here ); diff --git a/src/containers/WorkspaceExport.js b/src/containers/WorkspaceExport.js index e1ac982c10aa03c0cff9451dd1c3fbb63bac98ab..069a52542815f19231609bdfe7e0f81997efbff7 100644 --- a/src/containers/WorkspaceExport.js +++ b/src/containers/WorkspaceExport.js @@ -1,6 +1,5 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/core/styles'; import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend/withPlugins'; import { WorkspaceExport } from '../components/WorkspaceExport'; @@ -17,18 +16,8 @@ const mapStateToProps = state => ({ exportableState: getExportableState(state), }); -/** - * Styles for the withStyles HOC - */ -const styles = theme => ({ - accordionTitle: { - padding: 0, - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, {}), withPlugins('WorkspaceExport'), ); diff --git a/src/containers/WorkspaceImport.js b/src/containers/WorkspaceImport.js index 01c22d154776e0dff666ef3a28af1df9f5daaf1a..c529bca89a68af3d2f688ed4f4617670cf141076 100644 --- a/src/containers/WorkspaceImport.js +++ b/src/containers/WorkspaceImport.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import { WorkspaceImport } from '../components/WorkspaceImport'; import * as actions from '../state/actions'; @@ -16,22 +15,8 @@ const mapDispatchToProps = { importConfig: actions.importMiradorState, }; -/** */ -const styles = theme => ({ - cancelBtn: { - color: theme.palette.text.primary, - }, - textField: { - width: '100%', - }, - textInput: { - fontFamily: 'monospace', - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(null, mapDispatchToProps), withPlugins('WorkspaceImport'), ); diff --git a/src/containers/WorkspaceMenu.js b/src/containers/WorkspaceMenu.js index afc303cfc921a212d52c8d5b9bcbc75b5a9b6398..41905b6feec9ca2bbfc429e1b0b0fcf8b247d7b6 100644 --- a/src/containers/WorkspaceMenu.js +++ b/src/containers/WorkspaceMenu.js @@ -4,9 +4,10 @@ import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { - getContainerId, getShowZoomControlsConfig, getThemeIds, + getShowZoomControlsConfig, getThemeIds, getWorkspace, } from '../state/selectors'; +import { withWorkspaceContext } from '../contexts/WorkspaceContext'; import { WorkspaceMenu } from '../components/WorkspaceMenu'; /** @@ -24,7 +25,6 @@ const mapDispatchToProps = { * @private */ const mapStateToProps = state => ({ - containerId: getContainerId(state), isWorkspaceAddVisible: getWorkspace(state).isWorkspaceAddVisible, showThemePicker: getThemeIds(state).length > 0, showZoomControls: getShowZoomControlsConfig(state), @@ -32,6 +32,7 @@ const mapStateToProps = state => ({ const enhance = compose( withTranslation(), + withWorkspaceContext, connect(mapStateToProps, mapDispatchToProps), withPlugins('WorkspaceMenu'), ); diff --git a/src/containers/WorkspaceMenuButton.js b/src/containers/WorkspaceMenuButton.js index 16e10d569baeeb4c63666db50e14fddf26cbeca5..b5f2518eb255db4cf81d47c6ab1b68d9ed602931 100644 --- a/src/containers/WorkspaceMenuButton.js +++ b/src/containers/WorkspaceMenuButton.js @@ -1,26 +1,10 @@ import { compose } from 'redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import { WorkspaceMenuButton } from '../components/WorkspaceMenuButton'; -/** - * - * @param theme - * @returns {{ctrlBtn: {margin: (number|string)}}} - */ -const styles = theme => ({ - ctrlBtn: { - margin: theme.spacing(1), - }, - ctrlBtnSelected: { - backgroundColor: theme.palette.action.selected, - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), withPlugins('WorkspaceMenuButton'), // further HOC ); diff --git a/src/containers/WorkspaceMosaic.js b/src/containers/WorkspaceMosaic.js index b66e2f4aa67fd4741712f7d9e131b052e693b9f6..4ab6529d059266edca949428ed7c0ff7a6c2b509 100644 --- a/src/containers/WorkspaceMosaic.js +++ b/src/containers/WorkspaceMosaic.js @@ -1,11 +1,9 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { getWorkspace } from '../state/selectors'; import * as actions from '../state/actions'; import { WorkspaceMosaic } from '../components/WorkspaceMosaic'; -import globalReactMosaicStyles from '../styles/react-mosaic-component'; /** * mapStateToProps - to hook up connect @@ -27,26 +25,7 @@ const mapStateToProps = state => ( */ const mapDispatchToProps = { updateWorkspaceMosaicLayout: actions.updateWorkspaceMosaicLayout }; -const styles = { - root: { - '& .mosaic-preview': { - boxShadow: 'none', - }, - '& .mosaic-tile': { - boxShadow: '0 1px 3px 0 rgba(0, 0, 0, .2), 0 1px 1px 0 rgba(0, 0, 0, .2), 0 2px 1px -1px rgba(0, 0, 0, .2)', - }, - '& .mosaic-window': { - boxShadow: 'none', - }, - '& .mosaic-window-toolbar': { - display: 'none !important', - }, - }, - ...globalReactMosaicStyles, -}; - const enhance = compose( - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('WorkspaceMosaic'), // further HOC go here diff --git a/src/containers/WorkspaceOptionsButton.js b/src/containers/WorkspaceOptionsButton.js index f17b51310f56018393b23b6593ff87da902f2857..5768c4cd2aae646aef2e586106f357cd3856fd59 100644 --- a/src/containers/WorkspaceOptionsButton.js +++ b/src/containers/WorkspaceOptionsButton.js @@ -1,24 +1,9 @@ import { compose } from 'redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { WorkspaceOptionsButton } from '../components/WorkspaceOptionsButton'; -/** - * - * @param theme - */ -const styles = theme => ({ - ctrlBtn: { - margin: theme.spacing(1), - }, - ctrlBtnSelected: { - backgroundColor: theme.palette.action.selected, - }, -}); - const enhance = compose( - withStyles(styles), withTranslation(), withPlugins('WorkspaceOptionsButton'), ); diff --git a/src/containers/WorkspaceOptionsMenu.js b/src/containers/WorkspaceOptionsMenu.js index 0b9a72029ed5cf4752a62229fe8b2ca0373dcaf3..c53628319097404bb0270c5ca81955d20155382a 100644 --- a/src/containers/WorkspaceOptionsMenu.js +++ b/src/containers/WorkspaceOptionsMenu.js @@ -1,18 +1,11 @@ import { compose } from 'redux'; -import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { getContainerId } from '../state/selectors'; +import { withWorkspaceContext } from '../contexts/WorkspaceContext'; import { WorkspaceOptionsMenu } from '../components/WorkspaceOptionsMenu'; -/** Used for connect */ -const mapStateToProps = state => ({ - containerId: getContainerId(state), -}); - -// containerId: getContainerId(state),/ const enhance = compose( withTranslation(), - connect(mapStateToProps, null), + withWorkspaceContext, ); export default enhance(WorkspaceOptionsMenu); diff --git a/src/containers/WorkspaceSelectionDialog.js b/src/containers/WorkspaceSelectionDialog.js index d14881af77bf8f0a0a894d50cf82b821910c037f..3305e5321672755ccd59391bc0d61d330e5b17a5 100644 --- a/src/containers/WorkspaceSelectionDialog.js +++ b/src/containers/WorkspaceSelectionDialog.js @@ -1,7 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import { WorkspaceSelectionDialog } from '../components/WorkspaceSelectionDialog'; import * as actions from '../state/actions'; @@ -23,60 +22,8 @@ const mapDispatchToProps = { */ const mapStateToProps = state => ({ workspaceType: getWorkspaceType(state) }); -/** */ -const styles = theme => ({ - card: { - backgroundColor: 'transparent', - borderRadius: '0', - boxShadow: '0 0 transparent', - display: 'flex', - }, - content: { - flex: '1 0 auto', - }, - details: { - display: 'flex', - flexDirection: 'column', - }, - headline: { - paddingBottom: '6px', - }, - list: { - '&active': { - outline: 'none', - }, - '&focus': { - outline: 'none', - }, - outline: 'none', - }, - media: { - flex: '0 0 120px', - height: '90px', - }, - menuItem: { - height: 'auto', - overflow: 'auto', - whiteSpace: 'inherit', - }, - root: { - '&:last-child': { - paddingBottom: '12px', - }, - paddingBottom: 0, - paddingTop: 0, - textAlign: 'left', - }, - svgIcon: { - flexShrink: 0, - height: '90px', - width: '120px', - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('WorkspaceSelectionDialog'), ); diff --git a/src/containers/ZoomControls.js b/src/containers/ZoomControls.js index 512ce860cbef74cb3534084c3d8ba9d0e06db0bc..1cc85d545509720db14d9af2c07a189a5e10b7a6 100644 --- a/src/containers/ZoomControls.js +++ b/src/containers/ZoomControls.js @@ -1,10 +1,9 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; -import { getShowZoomControlsConfig, getViewer } from '../state/selectors'; +import { getViewer } from '../state/selectors'; import { ZoomControls } from '../components/ZoomControls'; /** @@ -14,7 +13,6 @@ import { ZoomControls } from '../components/ZoomControls'; */ const mapStateToProps = (state, { windowId }) => ( { - showZoomControls: getShowZoomControlsConfig(state), viewer: getViewer(state, { windowId }), } ); @@ -26,33 +24,8 @@ const mapStateToProps = (state, { windowId }) => ( */ const mapDispatchToProps = { updateViewport: actions.updateViewport }; -/** - * - * @param theme - * @returns {{zoom_controls: {position: string, right: number}, - * ListItem: {paddingBottom: number, paddingTop: number}}} - */ -const styles = theme => ({ - divider: { - borderRight: '1px solid #808080', - display: 'inline-block', - height: '24px', - margin: '12px 6px', - }, - ListItem: { - paddingBottom: 0, - paddingTop: 0, - }, - zoom_controls: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - }, -}); - const enhance = compose( withTranslation(), - withStyles(styles), connect(mapStateToProps, mapDispatchToProps), withPlugins('ZoomControls'), ); diff --git a/src/contexts/WorkspaceContext.js b/src/contexts/WorkspaceContext.js new file mode 100644 index 0000000000000000000000000000000000000000..6cf461e6b9f59591eb9adffb5eb1d186ceae8a6a --- /dev/null +++ b/src/contexts/WorkspaceContext.js @@ -0,0 +1,36 @@ +import { + createContext, useContext, useState, useEffect, forwardRef, +} from 'react'; + +const WorkspaceContext = createContext({ current: document.body }); + +/** + * @returns HOC that injects the workspace ref into the component + */ +export const withWorkspaceContext = (Component) => { + /** + * Wrap the component with the context + */ + function ContextHoc(props, ref) { + const workspaceContext = useContext(WorkspaceContext); + + const [workspaceRef, setWorkspaceRef] = useState(); + + useEffect(() => { + setWorkspaceRef(workspaceContext); + }, [workspaceContext]); + + const passDownProps = { + ...props, + ...(ref ? { ref } : {}), + }; + + return <Component container={workspaceRef} {...passDownProps} />; + } + + const whatever = forwardRef(ContextHoc); + whatever.displayName = `WithWorkspaceContext(${Component.displayName || Component.name || 'Component'})`; + return whatever; +}; + +export default WorkspaceContext; diff --git a/src/extend/pluginMapping.js b/src/extend/pluginMapping.js index efc449252abff501d89a04062516592fe7c2dd9b..7ddeefd6bb9c6e88f9cb1134abb0f66dffef05c6 100644 --- a/src/extend/pluginMapping.js +++ b/src/extend/pluginMapping.js @@ -18,7 +18,7 @@ import CompanionWindowRegistry from '../lib/CompanionWindowRegistry'; */ export function createTargetToPluginMapping(plugins) { return plugins.reduce((map, plugin) => ( - update(map, [plugin.target, plugin.mode], x => [...x || [], plugin]) + update(map, [plugin.target, plugin.mode], x => [...(x || []), plugin]) ), {}); } diff --git a/src/i18n.js b/src/i18n.js index 7176887c179ebad99fe89ce4b0c88564c1853a1b..797b765458068803f81bd40a9b9f455a1e9bff89 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -3,6 +3,7 @@ import { initReactI18next } from 'react-i18next'; import ar from './locales/ar/translation.json'; import de from './locales/de/translation.json'; import en from './locales/en/translation.json'; +import fa from './locales/fa/translation.json'; import zhCn from './locales/zhCn/translation.json'; import zhTw from './locales/zhTw/translation.json'; import fr from './locales/fr/translation.json'; @@ -28,6 +29,7 @@ function createI18nInstance() { bg, de, en, + fa, fr, it, ja, diff --git a/src/lib/MiradorCanvas.js b/src/lib/MiradorCanvas.js index 01a1eef4bd255171659b59323e539b5a3ab9a165..6e17fad668cfb15c54cafa6e7c9b1b5adc75f8ac 100644 --- a/src/lib/MiradorCanvas.js +++ b/src/lib/MiradorCanvas.js @@ -1,6 +1,6 @@ import flatten from 'lodash/flatten'; import flattenDeep from 'lodash/flattenDeep'; -import { Canvas } from 'manifesto.js'; +import { Canvas, AnnotationPage, Annotation } from 'manifesto.js'; /** * MiradorCanvas - adds additional, testable logic around Manifesto's Canvas * https://iiif-commons.github.io/manifesto/classes/_canvas_.manifesto.canvas.html @@ -86,7 +86,6 @@ export default class MiradorCanvas { const resources = flattenDeep([ this.canvas.getContent().map(i => i.getBody()), ]); - return flatten(resources.filter((resource) => resource.getProperty('type') === 'Video')); } @@ -100,7 +99,7 @@ export default class MiradorCanvas { } /** */ - get vttContent() { + get v2VttContent() { const resources = flattenDeep([ this.canvas.getContent().map(i => i.getBody()), ]); @@ -108,6 +107,19 @@ export default class MiradorCanvas { return flatten(resources.filter((resource) => resource.getProperty('format') === 'text/vtt')); } + /** IIIF v3 captions are stored as 'supplementing' Annotations rather than in the resource content itself */ + get v3VttContent() { + const resources = flattenDeep(this.canvasAnnotationPages.map(annoPage => { + const manifestoAnnoPage = new AnnotationPage(annoPage, this.canvas.options); + return manifestoAnnoPage.getItems().map(item => { + const manifestoAnnotation = new Annotation(item, this.canvas.options); + return manifestoAnnotation.getBody(); + }); + })); + + return flatten(resources.filter((resource) => resource.getProperty('format') === 'text/vtt')); + } + /** */ get resourceAnnotations() { return flattenDeep([ diff --git a/src/locales/ar/translation.json b/src/locales/ar/translation.json index 5702f4b7cdc983f460bece9cee12a397cc753342..23f4984a8df27683d158ecdffef13e94f31a6f54 100644 --- a/src/locales/ar/translation.json +++ b/src/locales/ar/translation.json @@ -26,7 +26,6 @@ "closeWindow": "إغلاق النافذة", "collapseSection": "تصغير جزء {{section}}", "collapseSidePanel": "تصغير الشريط الجانبي", - "itemList": "قائمة العناصر", "continue": "استمر", "copy": "نسخ", "currentItem": "العنصر الحالي", @@ -35,7 +34,6 @@ "currentItem_2/2": "يمين", "dark": "موضوع داكن", "dismiss": "تجاهل", - "highlightAllAnnotations": "تحديد الكل", "downloadExport": "تصدير مساحة العمل", "downloadExportWorkspace": "تصدير مساحة العمل", "elastic": "مرن", @@ -50,20 +48,22 @@ "fullScreen": "شاشة كاملة", "gallery": "المعرض", "hideZoomControls": "إخفاء ضوابط التكبير", + "highlightAllAnnotations": "تحديد الكل", "iiif_homepage": "عن هذه المصدر", "iiif_manifest": "IIIF قائمة ", "iiif_renderings": "تنسيقات بديلة", "iiif_seeAlso": "أنظر أيضا", - "import" : "استورد ", + "import": "استورد ", "importWorkspace": "استيراد مساحة العمل", "importWorkspaceHint": "قم بلصق تركيبة Mirador 3 ليتم استيراده", "item": "العنصر: {{label}}", + "itemList": "قائمة العناصر", "language": "اللغة", "layer_hide": "إخفاء الطبقة", "layer_move": "تحريك الطبقة", + "layer_moveToTop": "حرك الطبقة إلى الأعلى", "layer_opacity": "تعتيم الطبقة", "layer_show": "إظهار الطبقة", - "layer_moveToTop": "حرك الطبقة إلى الأعلى", "layers": "طبقات", "light": "موضوع فاتح", "links": "الروابط", diff --git a/src/locales/bg/translation.json b/src/locales/bg/translation.json index 687e52c5dec8e4e95b2954b84b2fe369765d5a21..fd3030bb3232493b8518ddcffbf737f01e323271 100644 --- a/src/locales/bg/translation.json +++ b/src/locales/bg/translation.json @@ -29,7 +29,6 @@ "collapseSection": "Свиване на секция \"{{section}}\"", "collapseSidePanel": "Свиване на страничен панел", "collection": "Колекция", - "itemList": "Списък с елементи", "continue": "Продължи", "copy": "Копиране", "currentItem": "Текущ елемент", @@ -39,7 +38,6 @@ "dark": "Тъмен режим", "digitizedView": "Цифровизиран преглед", "dismiss": "Отхвърляне", - "highlightAllAnnotations": "Маркиране на всичко", "displayNoAnnotations": "Изчистване на маркирането", "downloadExport": "Експорт на работното пространство", "downloadExportWorkspace": "Експорт на работното пространство", @@ -57,23 +55,25 @@ "fullScreen": "Цял екран", "gallery": "Галерия", "hideZoomControls": "Скриване на контролите за мащабиране", + "highlightAllAnnotations": "Маркиране на всичко", "iiif_homepage": "За този ресурс", "iiif_manifest": "IIIF манифест", "iiif_related": "Свързано", "iiif_renderings": "Алтернативни формати", "iiif_seeAlso": "Вижте също", - "import" : "Импорт", + "import": "Импорт", "importWorkspace": "Импорт на работно пространство", "importWorkspaceHint": "Поставете Mirador 3 конфигурация за импорт", "item": "Елемент: {{label}}", + "itemList": "Списък с елементи", "jsError": "Технически детайли", "jsStack": "{{ stack }}", "language": "Език", "layer_hide": "Скриване на слой", "layer_move": "Преместване на слой", + "layer_moveToTop": "Преместване на слой най-отгоре", "layer_opacity": "Прозрачност на слой", "layer_show": "Показване на слой", - "layer_moveToTop": "Преместване на слой най-отгоре", "layers": "Слоеве", "light": "Светъл режим", "links": "Връзки", @@ -124,9 +124,9 @@ "searchSubmitAria": "Търси", "searchTitle": "Търсене", "selectWorkspaceMenu": "Посочете тип работно пространство", + "showCollection": "Показване на колекция", "showingNumAnnotations": "Показване на {{number}} анотация", "showingNumAnnotations_plural": "Показване на {{number}} анотации", - "showCollection": "Показване на колекция", "showZoomControls": "Показване на контролите за мащабиране", "sidebarPanelsNavigation": "Навигация на страничните панели", "single": "Единичен", @@ -153,9 +153,9 @@ "windowPluginButtons": "Настройки", "windowPluginMenu": "Настройки на прозорците", "workspace": "Работно пространство", - "workspaceNavigation": "Навигация на работното пространство", "workspaceFullScreen": "Цял екран", "workspaceMenu": "Настройки на работното пространство", + "workspaceNavigation": "Навигация на работното пространство", "workspaceOptions": "Опции за работното пространство", "workspaceSelectionTitle": "Изберете тип на работно пространство", "zoomIn": "Увеличаване", diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index 6f158c826faa4fa16a644e6d907f7dbdd465d203..7bbadf2c18288a5ba372b49e659595b3f90616a5 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -27,7 +27,6 @@ "closeWindow": "Fenster schließen", "collapseSection": "Bereich \"{{section}}\" zuklappen", "collapseSidePanel": "Seitenleiste zuklappen", - "itemList": "Kompaktliste", "continue": "Fortfahren", "copy": "Kopieren", "currentItem": "Aktuelles Objekt", @@ -37,7 +36,6 @@ "dark": "Dunkel", "digitizedView": "Digitalisat-Ansicht", "dismiss": "Verwerfen", - "highlightAllAnnotations": "Alle hervorheben", "displayNoAnnotations": "Keine hervorheben", "downloadExport": "Arbeitsfläche exportieren", "downloadExportWorkspace": "Arbeitsfläche exportieren", @@ -55,23 +53,25 @@ "fullScreen": "Vollbild", "gallery": "Galerie", "hideZoomControls": "Zoomsteuerung verbergen", + "highlightAllAnnotations": "Alle hervorheben", "iiif_homepage": "Über diese Ressource", "iiif_manifest": "IIIF-Manifest", "iiif_related": "Verwandtes", "iiif_renderings": "Alternative Formate", "iiif_seeAlso": "Siehe auch", - "import" : "Importieren", + "import": "Importieren", "importWorkspace": "Arbeitsfläche importieren", "importWorkspaceHint": "Fügen Sie eine Mirador-3-Konfiguration ein, die importiert werden soll.", "item": "Objekt: {{label}}", + "itemList": "Kompaktliste", "jsError": "Technische Details", "jsStack": "{{ stack }}", "language": "Sprache", "layer_hide": "Ebene verbergen", "layer_move": "Ebene verschieben", + "layer_moveToTop": "Ebene ganz nach vorn bringen", "layer_opacity": "Ebenendeckkraft", "layer_show": "Ebene anzeigen", - "layer_moveToTop": "Ebene ganz nach vorn bringen", "layers": "Ebenen", "light": "Hell", "links": "Links", @@ -141,9 +141,9 @@ "windowPluginButtons": "Optionen", "windowPluginMenu": "Fensteroptionen", "workspace": "Arbeitsfläche", - "workspaceNavigation": "Arbeitsflächensteuerung", "workspaceFullScreen": "Vollbild", "workspaceMenu": "Arbeitsflächenmenü", + "workspaceNavigation": "Arbeitsflächensteuerung", "workspaceOptions": "Arbeitsflächenoptionen", "workspaceSelectionTitle": "Bitte wählen Sie einen Arbeitsflächentyp", "zoomIn": "Vergrößern", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 29471e77cec96dad0281ca49f372ecc5e6cfecb0..e65806308a6924a77b04e82155e0b317c4f1b3b3 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -29,7 +29,6 @@ "collapseSection": "Collapse \"{{section}}\" section", "collapseSidePanel": "Collapse sidebar", "collection": "Collection", - "itemList": "Item list", "continue": "Continue", "copy": "Copy", "currentItem": "Current item", @@ -39,7 +38,6 @@ "dark": "Dark theme", "digitizedView": "Digitized view", "dismiss": "Dismiss", - "highlightAllAnnotations": "Highlight all", "displayNoAnnotations": "Highlight none", "downloadExport": "Export workspace", "downloadExportWorkspace": "Export workspace", @@ -57,23 +55,25 @@ "fullScreen": "Full Screen", "gallery": "Gallery", "hideZoomControls": "Hide zoom controls", + "highlightAllAnnotations": "Highlight all", "iiif_homepage": "About this resource", "iiif_manifest": "IIIF manifest", "iiif_related": "Related", "iiif_renderings": "Alternate formats", "iiif_seeAlso": "See also", - "import" : "Import", + "import": "Import", "importWorkspace": "Import workspace", "importWorkspaceHint": "Paste a Mirador 3 configuration to be imported", "item": "Item: {{label}}", + "itemList": "Item list", "jsError": "Technical details", "jsStack": "{{ stack }}", "language": "Language", "layer_hide": "Hide layer", "layer_move": "Move layer", + "layer_moveToTop": "Move layer to top", "layer_opacity": "Layer opacity", "layer_show": "Show layer", - "layer_moveToTop": "Move layer to top", "layers": "Layers", "light": "Light theme", "links": "Links", @@ -132,9 +132,9 @@ "searchTitle": "Search", "searchPlaceholderAnnotation" : "Filter...", "selectWorkspaceMenu": "Select workspace type", + "showCollection": "Show collection", "showingNumAnnotations_one": "Showing {{number}} annotation", "showingNumAnnotations_other": "Showing {{number}} annotations", - "showCollection": "Show collection", "showZoomControls": "Show zoom controls", "sidebarPanelsNavigation": "Sidebar panels navigation", "single": "Single", @@ -161,9 +161,9 @@ "windowPluginButtons": "Options", "windowPluginMenu": "Window options", "workspace": "Workspace", - "workspaceNavigation": "Workspace navigation", "workspaceFullScreen": "Full screen", "workspaceMenu": "Workspace settings", + "workspaceNavigation": "Workspace navigation", "workspaceOptions": "Workspace options", "workspaceSelectionTitle": "Select a workspace type", "zoomIn": "Zoom in", diff --git a/src/locales/fa/translation.json b/src/locales/fa/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..21cd432b4c50d97551bf2ba19699a05b0a9ac685 --- /dev/null +++ b/src/locales/fa/translation.json @@ -0,0 +1,165 @@ +{ + "translation": { + "aboutMirador": "در بارهٔ پروژه Mirador", + "aboutThisItem": "در بارهٔ این مورد", + "addedFromUrl": "(از آدرس اینترنتی ازافه شده)", + "addManifestUrl": "مکان منبع", + "addManifestUrlHelp": "آدرس اینترنتی منبع IIIF ", + "addResource": "منبع را ازافه کن", + "annotationCanvasLabel_1/1": "مورد: [{{label}}]", + "annotationCanvasLabel_1/2": "چپ: [{{label}}]", + "annotationCanvasLabel_2/2": "راست: [{{label}}]", + "annotations": "حاشیه نویسی ها", + "attribution": "نسبت دادن", + "attributionTitle": "حقوق", + "authenticationFailed": "تصدیق هویت ناموفق بود.", + "authenticationRequired": "برای دسترسی کامل نیاز به احراز هویت", + "backToResults": "بازگشت به نتایج", + "book": "کتاب", + "bottom": "پایان", + "cancel": "لغو", + "canvasIndex": "فهرست راهنما", + "changeTheme": "تغیر زمینه", + "clearSearch": "پاک کن", + "close": "ببند", + "closeAddResourceForm": "پرسشنامه را ببند", + "closeAddResourceMenu": "فهرست منابع را ببند", + "closeCompanionWindow": "پانل را ببند", + "closeWindow": "پنجره را ببند", + "collapseSection": "بخش \"{{section}}\" را کوچک کنید", + "collapseSidePanel": "نوار کناری را کوچک کن", + "collection": "مجموعه", + "continue": "ادامه", + "copy": "کپی", + "currentItem": "مورد فعلی", + "currentItem_1/1": "مورد فعلی", + "currentItem_1/2": "چپ", + "currentItem_2/2": "راست", + "dark": "زمینه تاریک", + "digitizedView": "نمای دیجیتالی شده", + "dismiss": "مرخص کن", + "displayNoAnnotations": "هیچ کدام را برجسته نکنید", + "downloadExport": "صدور فضای کاری", + "downloadExportWorkspace": "صدور فضای کاری", + "elastic": "کشدار", + "elasticDescription": "پنجره ها را آزادانه در یک فضای کاری نامحدود حرکت دهید و اندازه کنید. پنجره ها می تواند همپوشانی داشته باشند.", + "emptyResourceList": "فهرست منابع شما خالی است", + "error": "خطا", + "errorDialogConfirm": "OK", + "errorDialogTitle": "خطایی اتفاق افتاد", + "exitFullScreen": "خروج از تمام صفحه", + "expandSection": "بخش \"{{section}}\" را باز کنید", + "expandSidePanel": "نوار کناری را باز کن", + "exportCopied": "پیکربندی فضای کاری در کلیپ بورد شما کپی شد", + "fetchManifest": "اضافه کن", + "fullScreen": "تمام صفحه", + "gallery": "آلبوم عکس", + "hideZoomControls": "کنترل های بزرگنما را مخفی کنید", + "highlightAllAnnotations": "همه را برجسته کنید", + "iiif_homepage": "در باره ی این منبع", + "iiif_manifest": "بیانیه ی IIIF", + "iiif_related": "مربوط", + "iiif_renderings": "فرمت های جایگزین", + "iiif_seeAlso": "همچنین ببینید", + "import": "وارد كنید", + "importWorkspace": "فضای کاری را وارد كنید", + "importWorkspaceHint": "پیکربندی Mirador 3 را برای وارد کردن جایگذاری کنید", + "item": "مورد: {{label}}", + "itemList": "فهرست موارد", + "jsError": "جزییات فنی", + "jsStack": "{{ stack }}", + "language": "زبان", + "layer_hide": "لایه را پنهان کنید", + "layer_move": "لایه را حرکت دهید", + "layer_moveToTop": "لایه را به بالا منتقل کنید", + "layer_opacity": "لایه کدورت", + "layer_show": "نمایش لایه", + "layers": "لایه ها", + "light": "زمینه رشن", + "links": "پیوندها", + "listAllOpenWindows": "پرش به پنجره", + "login": "وارد به سیستم", + "logout": "خروج از سیستم", + "manifestError": "منبع اضافه نمیشود :", + "maximizeWindow": "پنجره را به حداکثر برسانید", + "minimizeWindow": "پنجره را به حداقل برسانید", + "mirador": "Mirador", + "miradorResources": "منابعی Mirador", + "miradorViewer": "بیننده Mirador", + "more": "بیشتر...", + "moreResults": "نتایج بیشتر", + "mosaic": "موزائیک", + "mosaicDescription": "جابجایی و اندازه پنجره ها نسبت به یکدیگر، در چارچوب قابل مشاهده.", + "moveCompanionWindowToBottom": "حرکت به سمت پایین", + "moveCompanionWindowToRight": "حرکت به سمت راست", + "multipartCollection": "مجموعه چند قسمتی", + "nextCanvas": "مورد بعدی", + "noItemSelected": "هیچ موردی انتخاب نشده است", + "numItems_one": "{{number}} مورد", + "numItems_other": "{{number}} موارد", + "off": "خاموش", + "openCompanionWindow_annotations": "حاشیه نویسی ها", + "openCompanionWindow_attribution": "حقوق", + "openCompanionWindow_canvas": "فهرست راهنما", + "openCompanionWindow_info": "اطلاعات", + "openCompanionWindow_layers": "لایه ها", + "openCompanionWindow_search": "جستجو", + "openInCompanionWindow": "در پنل جداگانه باز کن", + "openWindows": "پنجره های باز", + "pagination": "{{total}} از {{current}}", + "position": "موقعیت", + "previewWindowTitle": "{{title}}", + "previousCanvas": "مورد قبلی", + "related": "مربوط", + "resource": "منبع", + "retry": "امتحان دوباره", + "right": "حقوق", + "rights": "مجوز", + "scroll": "پیمایش", + "searchInputLabel": "عبارات جستجو", + "searchNextResult": "نتیجه بعدی", + "searchNoResults": "نتیجه ای پیدا نشد", + "searchPreviousResult": "نتیجه قبلی", + "searchResultsRemaining": "{{numLeft}} باقی مانده", + "searchSubmitAria": "ارسال جستجو", + "searchTitle": "جستجو", + "selectWorkspaceMenu": "نوع فضای کار را انتخاب کنید", + "showCollection": "نمایش مجموعه", + "showingNumAnnotations_one": "نمایش {{number}} حاشیه نویسی", + "showingNumAnnotations_other": "نمایش {{number}} حاشیه نویسی", + "showZoomControls": "نمایش کنترل های بزرگنمایی", + "sidebarPanelsNavigation": "پانل های ناوبری نوار کناری", + "single": "واحد", + "startHere": "از اینجا شروع کنید", + "suggestSearch": "این سند را برای \"{{ query }}\" جستجوی کنید", + "tableOfContentsList": "فهرست مطالب", + "theme": "زمینه", + "thumbnailList": "فهرست تصاویر بندانگشتی", + "thumbnailNavigation": "تصاویر بندانگشتی", + "thumbnails": "تصاویر بندانگشتی", + "toggleWindowSideBar": "نوار کناری را تغییر دهید", + "totalCollections_one": "{{count}} مجموعه", + "totalCollections_other": "{{count}} مجموعات", + "totalManifests_one": "{{count}} بیانیه", + "totalManifests_other": "{{count}} بیانیه ها", + "tryAgain": "دوباره امتحان کنید", + "untitled": "[بدون عنوان]", + "view": "مشاهده", + "viewWorkspaceConfiguration": "مشاهده پیکربندی فضای کاری", + "welcome": "به Mirador خوش آمدید", + "window": "پنجره: {{label}}", + "windowMenu": "نماهای پنجره و نمایش تصاویر کوچک", + "windowNavigation": "جهت یابی پنجره", + "windowPluginButtons": "گزینه ها", + "windowPluginMenu": "گزینه های پنجره", + "workspace": "فضای کار", + "workspaceFullScreen": "تمام صفحه", + "workspaceMenu": "تنظیمات فضای کاری", + "workspaceNavigation": "جهت یابی فضای کاری", + "workspaceOptions": "گزینه های فضای کاری", + "workspaceSelectionTitle": "نوع فضای کاری را انتخاب کنید", + "zoomIn": "بزرگنمایی", + "zoomOut": "کوچک نمایی", + "zoomReset": " تنظیم بزرگنما" + } +} diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index 45ce26156c700987ae2c0c192c1fbff6b73a17d2..7b494e4d34436ba75d914d5557c40f89fd9c7146 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -27,7 +27,6 @@ "closeWindow": "Fermer cette fenêtre", "collapseSection": "Replier la section \"{{section}}\"", "collapseSidePanel": "Replier le panneau", - "itemList": "Liste compacte", "continue": "Continuer", "copy": "Copier", "currentItem": "Image courante", @@ -37,7 +36,6 @@ "dark": "Thème sombre", "digitizedView": "Vue de l'objet numérique", "dismiss": "Rejeter", - "highlightAllAnnotations": "Afficher tout", "displayNoAnnotations": "Masquer tout", "downloadExport": "Exporter l'espace de travail", "downloadExportWorkspace": "Exporter l'espace de travail", @@ -55,22 +53,24 @@ "fullScreen": "Plein écran", "gallery": "Galerie", "hideZoomControls": "Désactiver les commandes de zoom", + "highlightAllAnnotations": "Afficher tout", "iiif_homepage": "A propos de cette ressource", "iiif_manifest": "Manifeste IIIF", "iiif_renderings": "Formats alternatifs", "iiif_seeAlso": "Voir aussi", - "import" : "Importer", + "import": "Importer", "importWorkspace": "Importer un espace de travail", "importWorkspaceHint": "Insérer une configuration Mirador 3 à importer", "item": "Item : {{label}}", + "itemList": "Liste compacte", "jsError": "Détails techniques", "jsStack": "{{ stack }}", "language": "Langue", "layer_hide": "Masquer le calque", "layer_move": "Déplacer le calque", + "layer_moveToTop": "Déplacer le calque en haut", "layer_opacity": "Opacité du calque", "layer_show": "Afficher le calque", - "layer_moveToTop": "Déplacer le calque en haut", "layers": "Calques", "light": "Thème clair", "links": "Liens", @@ -127,9 +127,9 @@ "searchSubmitAria": "Lancer la recherche", "searchTitle": "Rechercher", "selectWorkspaceMenu": "Changer de type d'espace de travail", + "showCollection": "Voir la collection", "showingNumAnnotations_one": "{{number}} annotation affichée", "showingNumAnnotations_other": "{{number}} annotations affichées", - "showCollection": "Voir la collection", "showZoomControls": "Activer les commandes de zoom", "sidebarPanelsNavigation": "Navigation dans les panneaux latéraux", "single": "Simple", @@ -155,9 +155,9 @@ "windowPluginButtons": "Options", "windowPluginMenu": "Autres options et outils", "workspace": "Espace de travail", - "workspaceNavigation": "Menu de l'espace de travail", "workspaceFullScreen": "Plein écran", "workspaceMenu": "Réglages de l'espace de travail", + "workspaceNavigation": "Menu de l'espace de travail", "workspaceOptions": "Options de l'espace de travail", "workspaceSelectionTitle": "Sélectionner un type d'espace de travail", "zoomIn": "Zoom avant", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index af66cfd047a6cab655b80a9fd71d378d21ab026b..682236548f2f50b8e72622ba1db62237f65ae545 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -2,10 +2,10 @@ "translation": { "aboutMirador": "Informazioni su Mirador", "aboutThisItem": "Informazioni sull'oggetto", + "addedFromUrl": "(Aggiunto dall'URL)", "addManifestUrl": "URL della risorsa", "addManifestUrlHelp": "L'URL di una risorsa IIIF", "addResource": "Aggiungi una risorsa", - "addedFromUrl": "(Aggiunto dall'URL)", "annotationCanvasLabel_1/1": "Oggetto: [{{label}}]", "annotationCanvasLabel_1/2": "Sinistra: [{{label}}]", "annotationCanvasLabel_2/2": "Destra: [{{label}}]", @@ -123,9 +123,9 @@ "searchTitle": "Cerca", "selectWorkspaceMenu": "Selezione il tipo di workspace", "showCollection": "Visualizza la collezione", - "showZoomControls": "Mostra i controlli di zoom", "showingNumAnnotations_one": "Sto mostrando {{number}} annotazioni", "showingNumAnnotations_other": "Visualizzando {{number}} annotazioni", + "showZoomControls": "Mostra i controlli di zoom", "sidebarPanelsNavigation": "Navigazione dei pannelli della barra laterale", "single": "Singolo", "startHere": "Inizia qui", diff --git a/src/locales/ja/translation.json b/src/locales/ja/translation.json index 4a95d691dd74321bf43f1ab3905a53312b8a2a7c..296ba2420ac0ba13d924f6e48aab85824655f5ce 100644 --- a/src/locales/ja/translation.json +++ b/src/locales/ja/translation.json @@ -29,7 +29,6 @@ "collapseSection": "{{section}} セクションを畳む", "collapseSidePanel": "サイドバーを畳む", "collection": "コレクション", - "itemList": "アイテム一覧", "continue": "続ける", "copy": "コピー", "currentItem": "現在のアイテム", @@ -39,7 +38,6 @@ "dark": "ダークなテーマ", "digitizedView": "デジタルビュー", "dismiss": "片付け", - "highlightAllAnnotations": "すべてを表示", "displayNoAnnotations": "アノテーションを非表示", "downloadExport": "ワークスペースをエクスポート", "downloadExportWorkspace": "ワークスペースをエクスポート", @@ -57,6 +55,7 @@ "fullScreen": "全画面", "gallery": "ギャラリー", "hideZoomControls": "zoom制御を隠す", + "highlightAllAnnotations": "すべてを表示", "iiif_homepage": "IIIFホームページ", "iiif_manifest": "IIIF マニフェスト", "iiif_renderings": "別の形式", @@ -65,14 +64,15 @@ "importWorkspace": "ワークスペースの取り込み", "importWorkspaceHint": "Mirador3の設定を貼り付け", "item": "アイテム: {{label}}", + "itemList": "アイテム一覧", "jsError": "技術的な詳細", "jsStack": "{{ stack }}", "language": "言語", "layer_hide": "レイヤーを隠す", "layer_move": "レイヤーを動かす", + "layer_moveToTop": "レイヤーをトップへ", "layer_opacity": "レイヤーの透過度", "layer_show": "レイヤーを表示", - "layer_moveToTop": "レイヤーをトップへ", "layers": "レイヤー", "light": "明るいテーマ", "links": "リンク", @@ -120,8 +120,8 @@ "searchSubmitAria": "検索", "searchTitle": "検索", "selectWorkspaceMenu": "ワークスペースタイプの選択", - "showingNumAnnotations_one": "アノテーション {{number}} を表示", "showCollection": "コレクションを表示", + "showingNumAnnotations_one": "アノテーション {{number}} を表示", "showZoomControls": "ズーム操作を表示", "sidebarPanelsNavigation": "サイドバーパネルの操作", "single": "単一", @@ -145,9 +145,9 @@ "windowPluginButtons": "オプション", "windowPluginMenu": "ウインドウオプション", "workspace": "ワークスペース", - "workspaceNavigation": "ワークスペースナビ", "workspaceFullScreen": "全画面", "workspaceMenu": "ワークスペースの設定", + "workspaceNavigation": "ワークスペースナビ", "workspaceOptions": "ワークスペースのオプション", "workspaceSelectionTitle": "ワークスペースのタイプを選択", "zoomIn": "ズームイン", diff --git a/src/locales/kr/translation.json b/src/locales/kr/translation.json index a084fe7cbd0baecda8edf2f1546be0b29666d11f..e87ecceac76e9f21ba786ebae4ac2b37a89ab5fb 100644 --- a/src/locales/kr/translation.json +++ b/src/locales/kr/translation.json @@ -29,7 +29,6 @@ "collapseSection": "\"{{section}}\" 섹션 접기", "collapseSidePanel": "사이드바 접기", "collection": "컬렉션", - "itemList": "아이템 목록", "continue": "계속하기", "copy": "복사", "currentItem": "현재 아이템", @@ -39,7 +38,6 @@ "dark": "다크 모드", "digitizedView": "디지털 뷰", "dismiss": "무시하기", - "highlightAllAnnotations": "모두 하이라이트하기", "displayNoAnnotations": "하이라이트 해제", "downloadExport": "작업공간 내보내기", "downloadExportWorkspace": "작업공간 내보내기", @@ -57,22 +55,24 @@ "fullScreen": "전체화면", "gallery": "갤러리", "hideZoomControls": "확대/축소 기능 숨기기", + "highlightAllAnnotations": "모두 하이라이트하기", "iiif_homepage": "해당 리소스에 대하여", "iiif_manifest": "IIIF 매니페스트", "iiif_renderings": "대체 포맷", "iiif_seeAlso": "참고", - "import" : "가져오기", + "import": "가져오기", "importWorkspace": "작업공간 가져오기", "importWorkspaceHint": "Mirador 3 환경설정 가져와 붙여넣기", "item": "아이템: {{label}}", + "itemList": "아이템 목록", "jsError": "기술적인 세부사항", "jsStack": "{{ stack }}", "language": "언어", "layer_hide": "레이어 숨기기", "layer_move": "레이어 움직이기", + "layer_moveToTop": "레이어를 위로 옮기기", "layer_opacity": "레이어 불투명도", "layer_show": "레이어 보이기", - "layer_moveToTop": "레이어를 위로 옮기기", "layers": "레이어", "light": "라이트 모드", "links": "링크", @@ -122,9 +122,9 @@ "searchSubmitAria": "검색하기", "searchTitle": "검색", "selectWorkspaceMenu": "작업공간 유형 선택", + "showCollection": "컬렉션 보이기", "showingNumAnnotations_one": "{{number}}개의 주석 나타내기", "showingNumAnnotations_other": "{{number}}개의 주석 나타내기", - "showCollection": "컬렉션 보이기", "showZoomControls": "확대/축소 기능 보이기", "sidebarPanelsNavigation": "사이드바 패널 탐색", "single": "한 개", @@ -151,9 +151,9 @@ "windowPluginButtons": "옵션", "windowPluginMenu": "윈도우 옵션", "workspace": "작업공간", - "workspaceNavigation": "작업공간 탐색", "workspaceFullScreen": "전체화면", "workspaceMenu": "작업공간 설정", + "workspaceNavigation": "작업공간 탐색", "workspaceOptions": "작업공간 옵션", "workspaceSelectionTitle": "작업공간 유형을 선택하세요", "zoomIn": "확대", diff --git a/src/locales/lt/translation.json b/src/locales/lt/translation.json index 6da0f5a3c491c8694333aebd3ce956c789852147..31cb7c7acc0a28f1c1bfce5a142ec99625cde419 100644 --- a/src/locales/lt/translation.json +++ b/src/locales/lt/translation.json @@ -27,7 +27,6 @@ "closeWindow": "Uždaryti langą", "collapseSection": "Suskleisti \"{{section}}\" sekciją", "collapseSidePanel": "Suskleisti šoninę juostą", - "itemList": "Įrašų sąrašas", "continue": "Tęsti", "copy": "Kopijuoti", "currentItem": "Dabartinis įrašas", @@ -37,7 +36,6 @@ "dark": "Tamsi tema", "digitizedView": "Skaitmenintas vaizdas", "dismiss": "Atmesti", - "highlightAllAnnotations": "Paryškinti viską", "displayNoAnnotations": "Slėpti viską", "downloadExport": "Išsaugoti darbalaukį", "downloadExportWorkspace": "Išsaugoti darbalaukį", @@ -55,6 +53,7 @@ "fullScreen": "Pilnas ekranas", "gallery": "Galerija", "hideZoomControls": "Slėpti priartinimo valdymą", + "highlightAllAnnotations": "Paryškinti viską", "iiif_homepage": "Apie šį šaltinį", "iiif_manifest": "IIIF manifestas", "iiif_renderings": "Kiti formatai", @@ -63,14 +62,15 @@ "importWorkspace": "Įkelti darbalaukį", "importWorkspaceHint": "Įterpkite Mirador 3 nustatymus įkėlimui", "item": "Įrašas: {{label}}", + "itemList": "Įrašų sąrašas", "jsError": "Techninė informacija", "jsStack": "{{ stack }}", "language": "Kalba", "layer_hide": "Slėpti sluoksnį", "layer_move": "Perkelti sluoksnį", + "layer_moveToTop": "Perkelti sluoksnį į viršų", "layer_opacity": "Sluoksnio nepermatomumas", "layer_show": "Rodyti sluoksnį", - "layer_moveToTop": "Perkelti sluoksnį į viršų", "layers": "Sluoksniai", "light": "Šviesi tema", "links": "Nuorodos", @@ -117,8 +117,8 @@ "searchSubmitAria": "Pateikti paieškos užklausą", "searchTitle": "Ieškoti", "selectWorkspaceMenu": "Pasirinkti darbalaukio tipą", - "showingNumAnnotations_one": "Rodoma {{number}} anotacija (-os)", "showCollection": "Rodyti kolekciją", + "showingNumAnnotations_one": "Rodoma {{number}} anotacija (-os)", "showZoomControls": "Rodyti priartinimo valdymą", "sidebarPanelsNavigation": "Šoninės juostos valdymas", "single": "Atskiras", @@ -142,9 +142,9 @@ "windowPluginButtons": "Pasirinkimai", "windowPluginMenu": "Langų pasirinkimai", "workspace": "Darbalaukis", - "workspaceNavigation": "Darbalaukio valdymas", "workspaceFullScreen": "Pilnas ekranas", "workspaceMenu": "Darbalaukio nustatymai", + "workspaceNavigation": "Darbalaukio valdymas", "workspaceOptions": "Darbalaukio pasirinkimai", "workspaceSelectionTitle": "Pasirinkite darbalaukio tipą", "zoomIn": "Priartinti", diff --git a/src/locales/nbNo/translation.json b/src/locales/nbNo/translation.json index 41a29c214b3dacc05cb31fcc9b97b1e7f8d9f52a..0d85a4a32eba7296e7c169515db90f4fc92b329b 100644 --- a/src/locales/nbNo/translation.json +++ b/src/locales/nbNo/translation.json @@ -29,7 +29,6 @@ "collapseSection": "Lukk seksjonen \"{{section}}\"", "collapseSidePanel": "Lukk sidemenyen", "collection": "Samling", - "itemList": "Objektliste", "continue": "Fortsett", "copy": "Kopiere", "currentItem": "Valgt objekt", @@ -39,7 +38,6 @@ "dark": "Mørkt tema", "digitizedView": "Digitalisert visning", "dismiss": "Lukk", - "highlightAllAnnotations": "Markér alle", "displayNoAnnotations": "Avmarkér alle", "downloadExport": "Eksportér arbeidsområde", "downloadExportWorkspace": "Eksportér arbeidsområde", @@ -57,6 +55,7 @@ "fullScreen": "Fullskjermsvisning", "gallery": "Galleri", "hideZoomControls": "Skjul zoomkontroll", + "highlightAllAnnotations": "Markér alle", "iiif_homepage": "Om denne ressursen", "iiif_manifest": "IIIF manifest", "iiif_renderings": "Alternativt format", @@ -65,14 +64,15 @@ "importWorkspace": "Importér arbeidsområde", "importWorkspaceHint": "Lim inn en Mirador 3 konfigurasjon for import", "item": "Objekt: {{label}}", + "itemList": "Objektliste", "jsError": "Tekniske detaljer", "jsStack": "{{ stack }}", "language": "Språk", "layer_hide": "Skjul lag", "layer_move": "Flytt lag", + "layer_moveToTop": "Flytt laget øverst", "layer_opacity": "Lag-gjennomsiktighet", "layer_show": "Vis lag", - "layer_moveToTop": "Flytt laget øverst", "layers": "Lag", "light": "Lyst tema", "links": "Lenker", @@ -122,9 +122,9 @@ "searchSubmitAria": "Søk", "searchTitle": "Søk", "selectWorkspaceMenu": "Velg arbeidsområde-type", + "showCollection": "Vis samling", "showingNumAnnotations_one": "Vis {{number}} annotasjon", "showingNumAnnotations_other": "Vis {{number}} annotasjoner", - "showCollection": "Vis samling", "showZoomControls": "Vis zoomkontroll", "sidebarPanelsNavigation": "Sidemeny-panel navigering", "single": "En og en", @@ -151,9 +151,9 @@ "windowPluginButtons": "Innstillinger", "windowPluginMenu": "Vindusinnstillinger", "workspace": "Arbeidsområde", - "workspaceNavigation": "Navigasjon i arbeidsområdet", "workspaceFullScreen": "Fullskjermsvisning", "workspaceMenu": "Innstillinger for arbeidsområdet", + "workspaceNavigation": "Navigasjon i arbeidsområdet", "workspaceOptions": "Flere valg for arbeidsområdet", "workspaceSelectionTitle": "Velg arbeidsområde-type", "zoomIn": "Zoom inn", diff --git a/src/locales/nl/translation.json b/src/locales/nl/translation.json index c2f3bb2051d1393b0ec1b525af44217ed6d5110e..448c55eba43760fc89b86d1235e12ebf9633a0d1 100644 --- a/src/locales/nl/translation.json +++ b/src/locales/nl/translation.json @@ -26,7 +26,6 @@ "closeWindow": "Sluit venster", "collapseSection": "Klap \"{{section}}\" sectie in", "collapseSidePanel": "Klap zijbalk in", - "itemList": "Compacte lijst", "continue": "Ga verder", "copy": "Kopieer", "currentItem": "Huidig item", @@ -35,7 +34,6 @@ "currentItem_2/2": "Rechts", "dark": "Donker thema", "dismiss": "Dismiss", - "highlightAllAnnotations": "Markeer alles", "downloadExport": "Exporteer workspace", "downloadExportWorkspace": "Exporteer workspace", "elastic": "Elastisch", @@ -50,14 +48,16 @@ "fullScreen": "Volledig scherm", "gallery": "Gallerij", "hideZoomControls": "Verberg zoomknoppen", + "highlightAllAnnotations": "Markeer alles", "iiif_homepage": "Over deze bron", "iiif_manifest": "IIIF-manifest", "iiif_renderings": "Alternatieve formaten", "iiif_seeAlso": "Zie ook", - "import" : "Importeer", + "import": "Importeer", "importWorkspace": "Importeer workspace", "importWorkspaceHint": "Plak een Mirador 3 configuratie die geïmporteerd moet worden", "item": "Item: {{label}}", + "itemList": "Compacte lijst", "language": "Taal", "light": "Licht thema", "links": "Links", diff --git a/src/locales/pl/translation.json b/src/locales/pl/translation.json index b8b69a88485145d5f51b3c3226716238d42f6e3b..c8ebd2fb9ee48e346bfe56c45d5c5b42f56e63d5 100644 --- a/src/locales/pl/translation.json +++ b/src/locales/pl/translation.json @@ -29,7 +29,6 @@ "collapseSection": "Zwiń sekcję \"{{section}}\"", "collapseSidePanel": "Zwiń panel boczny", "collection": "Zbiór", - "itemList": "Lista pozycji", "continue": "Kontynuuj", "copy": "Kopiuj", "currentItem": "Bieżąca pozycja", @@ -39,7 +38,6 @@ "dark": "Ciemny motyw", "digitizedView": "Widok zdigitalizowany", "dismiss": "Odrzuć", - "highlightAllAnnotations": "Podświetl wszystko", "displayNoAnnotations": "Nie podświetlaj", "downloadExport": "Eksportuj obszar roboczy", "downloadExportWorkspace": "Eksportuj obszar roboczy", @@ -57,22 +55,24 @@ "fullScreen": "Pełny ekran", "gallery": "Galeria", "hideZoomControls": "Ukryj kontrolki powiększenia", + "highlightAllAnnotations": "Podświetl wszystko", "iiif_homepage": "O tym zasobie", "iiif_manifest": "Manifest IIIF", "iiif_renderings": "Alternatywne formaty", "iiif_seeAlso": "Zobacz też", - "import" : "Importuj", + "import": "Importuj", "importWorkspace": "Importuj obszar roboczy", "importWorkspaceHint": "Wklej konfigurację Mirador 3 do zaimportowania", "item": "Pozycja: {{label}}", + "itemList": "Lista pozycji", "jsError": "Szczegóły techniczne", "jsStack": "{{ stack }}", "language": "Język", "layer_hide": "Ukryj warstwę", "layer_move": "Przenieś warstwę", + "layer_moveToTop": "Przenieś warstwę na górę", "layer_opacity": "Krycie warstwy", "layer_show": "Pokaż warstwę", - "layer_moveToTop": "Przenieś warstwę na górę", "layers": "Warstwy", "light": "Jasny motyw", "links": "Linki", @@ -122,9 +122,9 @@ "searchSubmitAria": "Wyszukaj", "searchTitle": "Wyszukaj", "selectWorkspaceMenu": "Wybierz typ obszaru roboczego", + "showCollection": "Pokaż zbiór", "showingNumAnnotations_one": "Wyświetlanie {{number}} adnotacji", "showingNumAnnotations_other": "Wyświetlanie {{number}} adnotacji", - "showCollection": "Pokaż zbiór", "showZoomControls": "Pokaż kontrolki powiększenia", "sidebarPanelsNavigation": "Nawigacja pasków panelu bocznego", "single": "Pojedynczy", @@ -151,9 +151,9 @@ "windowPluginButtons": "Opcje", "windowPluginMenu": "Opcje okna", "workspace": "Obszar roboczy", - "workspaceNavigation": "Nawigacja obszaru roboczego", "workspaceFullScreen": "Pełny ekran", "workspaceMenu": "Ustawienia obszaru roboczego", + "workspaceNavigation": "Nawigacja obszaru roboczego", "workspaceOptions": "Opcje obszaru roboczego", "workspaceSelectionTitle": "Wybierz typ obszaru roboczego", "zoomIn": "Przybliż", diff --git a/src/locales/ptBr/translation.json b/src/locales/ptBr/translation.json index f807f4c4c4c7446bc2fbbc1664f50af3e945aca1..f4d9e6341169a9c06e91348311b4fde11b3f4490 100644 --- a/src/locales/ptBr/translation.json +++ b/src/locales/ptBr/translation.json @@ -26,7 +26,6 @@ "closeWindow": "Fechar janela", "collapseSection": "Suprimir seção \"{{section}}\"", "collapseSidePanel": "Suprimir barra lateral", - "itemList": "Lista compacta", "continue": "Continuar", "copy": "Copiar", "currentItem": "Item atual", @@ -35,7 +34,6 @@ "currentItem_2/2": "Direita", "dark": "Tema escuro", "dismiss": "Cancelar", - "highlightAllAnnotations": "Exibir todas", "downloadExport": "Exportar área de trabalho", "downloadExportWorkspace": "Exportar área de trabalho", "elastic": "Elástico", @@ -50,14 +48,16 @@ "fullScreen": "Tela cheia", "gallery": "Galeria", "hideZoomControls": "Ocultar controles de zoom", + "highlightAllAnnotations": "Exibir todas", "iiif_homepage": "Sobre esse conteúdo", "iiif_manifest": "Manifesto IIIF", "iiif_renderings": "Outros formatos", "iiif_seeAlso": "Veja também", - "import" : "Importar", + "import": "Importar", "importWorkspace": "Importar área de trabalho", "importWorkspaceHint": "Cole uma configuração do Mirador 3 para importar", "item": "Item: {{label}}", + "itemList": "Lista compacta", "language": "Idioma", "light": "Tema claro", "links": "Links", diff --git a/src/locales/sr/translation.json b/src/locales/sr/translation.json index 4025124737bc333767ba5cb6a7686f64b62b0211..80d40353d0f13e7a6873ff830bdb6c93745c7fbd 100644 --- a/src/locales/sr/translation.json +++ b/src/locales/sr/translation.json @@ -29,7 +29,6 @@ "collapseSection": "Сакријте \"{{section}}\" секцију", "collapseSidePanel": "Сакријте", "collection": "Колекција", - "itemList": "Листа страница", "continue": "Наставите", "copy": "Копирајте", "currentItem": "Тренутна страница", @@ -39,7 +38,6 @@ "dark": "Тамна тема", "digitizedView": "Приказ дигиталног објекта", "dismiss": "Откажите", - "highlightAllAnnotations": "Приказ свих", "displayNoAnnotations": "Без приказа", "downloadExport": "Извезите радно окружење", "downloadExportWorkspace": "Извезите радно окружење", @@ -57,22 +55,24 @@ "fullScreen": "Приказ у пуном екрану", "gallery": "Галерија", "hideZoomControls": "Сакријте контроле зума", + "highlightAllAnnotations": "Приказ свих", "iiif_homepage": "О ресурсу", "iiif_manifest": "IIIF manifest", "iiif_renderings": "Додатни формати", "iiif_seeAlso": "Погледајте и", - "import" : "Увезите", + "import": "Увезите", "importWorkspace": "Увезите радно окружење", "importWorkspaceHint": "Убаците конфигурацију Mirador 3 како би могли да је увезете", "item": "Страница: {{label}}", + "itemList": "Листа страница", "jsError": "Технички детаљи", "jsStack": "{{ stack }}", "language": "Језик приказа", "layer_hide": "Сакријте слој", "layer_move": "Померите слој", + "layer_moveToTop": "Померите слој на врх", "layer_opacity": "Видљивост слоја", "layer_show": "Приказ слоја", - "layer_moveToTop": "Померите слој на врх", "layers": "Слојеви", "light": "Светла тема", "links": "Линкови", @@ -120,8 +120,8 @@ "searchSubmitAria": "Претражите", "searchTitle": "Претрага", "selectWorkspaceMenu": "Изаберите тип радног окружења", - "showingNumAnnotations_one": "Приказ {{number}} анотација/је", "showCollection": "Прикажи колекцију", + "showingNumAnnotations_one": "Приказ {{number}} анотација/је", "showZoomControls": "Приказ контрола зума", "sidebarPanelsNavigation": "Навигација сајдбар панела", "single": "Појединачно", diff --git a/src/locales/sv/translation.json b/src/locales/sv/translation.json index f752f5252f3a47beaccad31ad2081b2cda9838a4..8f4afca5326055186aebe8f686d3a42c7d68f931 100644 --- a/src/locales/sv/translation.json +++ b/src/locales/sv/translation.json @@ -29,7 +29,6 @@ "collapseSection": "Stäng sektionen \"{{section}}\"", "collapseSidePanel": "Stäng sidofält", "collection": "Samling", - "itemList": "Objektlista", "continue": "Fortsätt", "copy": "Kopiera", "currentItem": "Aktuellt objekt", @@ -39,7 +38,6 @@ "dark": "Mörkt tema", "digitizedView": "Digitaliserad vy", "dismiss": "Stäng", - "highlightAllAnnotations": "Markera alla", "displayNoAnnotations": "Avmarkera alla", "downloadExport": "Exportera arbetsyta", "downloadExportWorkspace": "Exportera arbetsyta", @@ -57,22 +55,24 @@ "fullScreen": "Helskärmsläge", "gallery": "Galleri", "hideZoomControls": "Dölj zoomkontroller", + "highlightAllAnnotations": "Markera alla", "iiif_homepage": "Om den här resursen", "iiif_manifest": "IIIF manifest", "iiif_renderings": "Alternativa format", "iiif_seeAlso": "Se även", - "import" : "Importera", + "import": "Importera", "importWorkspace": "Importera arbetsyta", "importWorkspaceHint": "Klistra in en Mirador 3 konfiguration att importera", "item": "Objekt: {{label}}", + "itemList": "Objektlista", "jsError": "Tekniska detaljer", "jsStack": "{{ stack }}", "language": "Språk", "layer_hide": "Dölj lager", "layer_move": "Flytta lager", + "layer_moveToTop": "Flytta lager till toppen", "layer_opacity": "Lageropacitet", "layer_show": "Visa lager", - "layer_moveToTop": "Flytta lager till toppen", "layers": "Lager", "light": "Ljust tema", "links": "Länkar", @@ -121,8 +121,8 @@ "searchSubmitAria": "Sök", "searchTitle": "Sök", "selectWorkspaceMenu": "Välj typ av arbetsyta", - "showingNumAnnotations_one": "Visar {{number}} noteringar", "showCollection": "Visa samling", + "showingNumAnnotations_one": "Visar {{number}} noteringar", "showZoomControls": "Visa zoomkontroller", "sidebarPanelsNavigation": "Sidofältspaneler navigering", "single": "En och en", @@ -146,9 +146,9 @@ "windowPluginButtons": "Inställningar", "windowPluginMenu": "Fönsterinställningar", "workspace": "Arbetsyta", - "workspaceNavigation": "Navigera i arbetsyta", "workspaceFullScreen": "Helskärmsläge", "workspaceMenu": "Inställningar för arbetsyta", + "workspaceNavigation": "Navigera i arbetsyta", "workspaceOptions": "Fler val för arbetsyta", "workspaceSelectionTitle": "Välj typ av arbetsyta", "zoomIn": "Zooma in", diff --git a/src/locales/vi/translation.json b/src/locales/vi/translation.json index 8a0b7b81390afb097ba721a1e27ff10347aaaa33..dc4fe5003f6cdce943092fec64bf4466268cc79a 100644 --- a/src/locales/vi/translation.json +++ b/src/locales/vi/translation.json @@ -29,7 +29,6 @@ "collapseSection": "Co sập {{section}} mục", "collapseSidePanel": "Co sập thanh bên", "collection": "Tuyển tập", - "itemList": "Danh sách khoản mục", "continue": "Tiếp tục", "copy": "Sao", "currentItem": "Khoản mục hiện thời", @@ -39,7 +38,6 @@ "dark": "Chủ đề tối", "digitizedView": "Cái nhìn số hoá", "dismiss": "Bác bỏ", - "highlightAllAnnotations": "Làm nổi bật tất", "displayNoAnnotations": "Không làm nổi bật", "downloadExport": "Vùng xuất khẩu", "downloadExportWorkspace": "Vùng xuất khẩu", @@ -57,22 +55,24 @@ "fullScreen": "Toàn màn hình", "gallery": "Phòng tranh", "hideZoomControls": "Ẩn điều khiển thu phóng", + "highlightAllAnnotations": "Làm nổi bật tất", "iiif_homepage": "Về tài nguyên này", "iiif_manifest": "Bản kê IIIF", "iiif_renderings": "Dạng thức luân phiên", "iiif_seeAlso": "Cũng xem", - "import" : "Nhập khẩu", + "import": "Nhập khẩu", "importWorkspace": "Vùng nhập khẩu", "importWorkspaceHint": "Dán cấu hình Mirador 3 để được nhập khẩu", "item": "Khoản mục: {{label}}", + "itemList": "Danh sách khoản mục", "jsError": "Chi tiết kĩ thuật", "jsStack": "{{ stack }}", "language": "Ngôn ngữ", "layer_hide": "Giấu tầng", "layer_move": "Chuyển tầng", + "layer_moveToTop": "Chuyển tầng lên đỉnh", "layer_opacity": "Làm mờ tầng", "layer_show": "Hiện tầng", - "layer_moveToTop": "Chuyển tầng lên đỉnh", "layers": "Tầng", "light": "Chủ đề sáng", "links": "Móc nối", @@ -120,8 +120,8 @@ "searchSubmitAria": "Đệ trình việc tìm", "searchTitle": "Tìm", "selectWorkspaceMenu": "Chọn kiểu vùng làm việc", - "showingNumAnnotations_one": "Hiện {{number}} chú giải", "showCollection": "Hiện bộ sưu tập", + "showingNumAnnotations_one": "Hiện {{number}} chú giải", "showZoomControls": "Hiện kiểm soát thu phóng", "sidebarPanelsNavigation": "Dẫn lái ngăn thanh bên", "single": "Chỉ một", @@ -145,9 +145,9 @@ "windowPluginButtons": "Tuỳ chọn", "windowPluginMenu": "Tuỳ chọn cửa sổ", "workspace": "Vùng làm việc", - "workspaceNavigation": "Dẫn lái vùng làm việc", "workspaceFullScreen": "Toàn màn hình", "workspaceMenu": "Thiết đặt vùng làm việc", + "workspaceNavigation": "Dẫn lái vùng làm việc", "workspaceOptions": "Tuỳ chọn vùng làm việc", "workspaceSelectionTitle": "Chọn kiểu vùng làm việc", "zoomIn": "Thu nhỏ", diff --git a/src/locales/zhCn/translation.json b/src/locales/zhCn/translation.json index 1aa8d12447ecd93294d9fb2c904126093840d32b..e1a5c15be1244c950852b8248e6a9a25e66038e4 100644 --- a/src/locales/zhCn/translation.json +++ b/src/locales/zhCn/translation.json @@ -29,7 +29,6 @@ "collapseSection": "关闭{{section}}分页", "collapseSidePanel": "关闭边栏", "collection": "集合", - "itemList": "标题列表", "continue": "继续", "copy": "复制", "currentItem": "当前条目", @@ -39,7 +38,6 @@ "dark": "暗色主题", "digitizedView": "数字视图", "dismiss": "关闭信息", - "highlightAllAnnotations": "高亮所有标注", "displayNoAnnotations": "不高亮", "downloadExport": "导出桌面排版", "downloadExportWorkspace": "导出桌面排版", @@ -57,22 +55,24 @@ "fullScreen": "全屏", "gallery": "画廊", "hideZoomControls": "隐藏缩放选项", + "highlightAllAnnotations": "高亮所有标注", "iiif_homepage": "主页", "iiif_manifest": "IIIF清单", "iiif_renderings": "其他格式", "iiif_seeAlso": "另见", - "import" : "导入", + "import": "导入", "importWorkspace": "导入桌面排版", "importWorkspaceHint": "在此贴上Mirador 3排版设定码", "item": "条目: {{label}}", + "itemList": "标题列表", "jsError": "技术细节", "jsStack": "{{ stack }}", "language": "语言", "layer_hide": "隐藏图层", "layer_move": "移动图层", + "layer_moveToTop": "将图层移到顶部", "layer_opacity": "图层不透明度", "layer_show": "显示图层", - "layer_moveToTop": "将图层移到顶部", "layers": "图层", "light": "亮色主题", "links": "链接", @@ -123,9 +123,9 @@ "searchSubmitAria": "提交搜索", "searchTitle": "搜索", "selectWorkspaceMenu": "选择桌面排版方式", + "showCollection": "显示集合", "showingNumAnnotations_one": "显示 {{number}} 项标注", "showingNumAnnotations_other": "显示 {{number}} 项标注", - "showCollection": "显示集合", "showZoomControls": "显示缩放选项", "sidebarPanelsNavigation": "切换边栏", "single": "单项", @@ -152,9 +152,9 @@ "windowPluginButtons": "选项", "windowPluginMenu": "窗口选项", "workspace": "桌面", - "workspaceNavigation": "工作区导航", "workspaceFullScreen": "全屏", "workspaceMenu": "桌面设定", + "workspaceNavigation": "工作区导航", "workspaceOptions": "桌面选项", "workspaceSelectionTitle": "选择桌面排版方式", "zoomIn": "放大", diff --git a/src/locales/zhTw/translation.json b/src/locales/zhTw/translation.json index 014abeaf224966acf9f44740e391e407ed30209c..b0b040bfb06501b955951138e9279cd681729a14 100644 --- a/src/locales/zhTw/translation.json +++ b/src/locales/zhTw/translation.json @@ -29,7 +29,6 @@ "collapseSection": "關閉{{section}}分頁", "collapseSidePanel": "關閉邊欄", "collection": "集合", - "itemList": "標題列表", "continue": "繼續", "copy": "複製", "currentItem": "目前物件", @@ -39,7 +38,6 @@ "dark": "暗色主題", "digitizedView": "數字視圖", "dismiss": "關閉信息", - "highlightAllAnnotations": "高亮所有標註", "displayNoAnnotations": "不高亮", "downloadExport": "滙出桌面排版", "downloadExportWorkspace": "滙出桌面排版", @@ -51,28 +49,30 @@ "errorDialogTitle": "發生錯誤", "exitFullScreen": "退出全螢幕", "expandSection": "開啟{{section}}分頁", - "exportCopied": "工作區配置被複製到你的剪貼板上了", "expandSidePanel": "開啟邊欄", + "exportCopied": "工作區配置被複製到你的剪貼板上了", "fetchManifest": "添加", "fullScreen": "全螢幕", "gallery": "矩列", "hideZoomControls": "隱藏縮放選項", + "highlightAllAnnotations": "高亮所有標註", "iiif_homepage": "有關此資源", "iiif_manifest": "IIIF清單", "iiif_renderings": "其他格式", "iiif_seeAlso": "另見", - "import" : "滙入", + "import": "滙入", "importWorkspace": "滙入桌面排版", "importWorkspaceHint": "在此貼上Mirador 3排版設定碼", "item": "物件: {{label}}", + "itemList": "標題列表", "jsError": "技術細節", "jsStack": "{{ stack }}", "language": "語言", "layer_hide": "隱藏圖層", "layer_move": "移動圖層", + "layer_moveToTop": "將圖層移到頂部", "layer_opacity": "圖層不透明度", "layer_show": "顯示圖層", - "layer_moveToTop": "將圖層移到頂部", "layers": "圖層", "light": "亮色主題", "links": "連結", @@ -123,9 +123,9 @@ "searchSubmitAria": "提交搜索", "searchTitle": "搜索", "selectWorkspaceMenu": "選擇桌面排版方式", + "showCollection": "顯示集合", "showingNumAnnotations_one": "顯示 {{number}} 項標註", "showingNumAnnotations_other": "顯示 {{number}} 項標註", - "showCollection": "顯示集合", "showZoomControls": "顯示縮放選項", "sidebarPanelsNavigation": "切換邊欄", "single": "單項", @@ -152,9 +152,9 @@ "windowPluginButtons": "選項", "windowPluginMenu": "視窗選項", "workspace": "桌面", - "workspaceNavigation": "工作區導航", "workspaceFullScreen": "全螢幕", "workspaceMenu": "桌面設定", + "workspaceNavigation": "工作區導航", "workspaceOptions": "桌面選項", "workspaceSelectionTitle": "選擇桌面排版方式", "zoomIn": "放大", diff --git a/src/state/actions/auth.js b/src/state/actions/auth.js index 582a472b09e1fcf2c5c4789f730f5052c826bc1a..0ba741d9cfadbacada5ab19bf4c8482cd641eee9 100644 --- a/src/state/actions/auth.js +++ b/src/state/actions/auth.js @@ -86,7 +86,9 @@ export function receiveAccessTokenFailure(authId, serviceId, error) { /** * resolveAccessTokenRequest - action creator * - * @param {Object} message + * @param {String} authServiceId + * @param {String} tokenServiceId + * @param {Object} json * @memberof ActionCreators */ export function resolveAccessTokenRequest(authServiceId, tokenServiceId, json) { diff --git a/src/state/actions/companionWindow.js b/src/state/actions/companionWindow.js index 0114b4af8520f1b7c08b6e831a6e947bd0670da5..58c5555f0f21af85de7fd53792f90e42fefc0601 100644 --- a/src/state/actions/companionWindow.js +++ b/src/state/actions/companionWindow.js @@ -77,3 +77,27 @@ export function toggleNode(windowId, id, nodeId) { }); }; } + +/** Update the expanded nodes state */ +export function expandNodes(windowId, id, nodeIds) { + return (dispatch, getState) => { + const state = getState(); + const expandedNodeIds = getManuallyExpandedNodeIds(state, { companionWindowId: id }, true); + const payload = {}; + + expandedNodeIds.forEach(nodeId => { + payload[nodeId] = { expanded: false }; + }); + + nodeIds.forEach(nodeId => { + payload[nodeId] = { expanded: true }; + }); + + return dispatch({ + id, + payload, + type: ActionTypes.TOGGLE_TOC_NODE, + windowId, + }); + }; +} diff --git a/src/state/createStore.js b/src/state/createStore.js index 9467f322c5535aab6d73abcd48d0aaed5b3dc9a1..c115859260c6db7cd5398b22b308bf9572cf5b32 100644 --- a/src/state/createStore.js +++ b/src/state/createStore.js @@ -6,7 +6,7 @@ import thunkMiddleware from 'redux-thunk'; import createSagaMiddleware from 'redux-saga'; import { combineReducers, createStore, applyMiddleware } from 'redux'; -import { composeWithDevTools } from 'redux-devtools-extension'; +import { composeWithDevTools } from '@redux-devtools/extension'; import createRootReducer from './reducers/rootReducer'; import getRootSaga from './sagas'; import settings from '../config/settings'; diff --git a/src/state/selectors/canvases.js b/src/state/selectors/canvases.js index 91674d44d935c24372db22a52073e7304ed851ba..d836cc58478ba4ee78037bc313622ab45c05d1b7 100644 --- a/src/state/selectors/canvases.js +++ b/src/state/selectors/canvases.js @@ -196,8 +196,12 @@ export const getVisibleCanvasCaptions = createSelector( [ getVisibleCanvases, ], - canvases => flatten(canvases - .map(canvas => new MiradorCanvas(canvas).vttContent)), + canvases => flatten(canvases.map(canvas => { + const miradorCanvas = new MiradorCanvas(canvas); + // prefer v3, fallback to v2, which can also be an empty array if no captions exist. + if (miradorCanvas.v3VttContent.length) return miradorCanvas.v3VttContent; + return miradorCanvas.v2VttContent; + })), ); export const getVisibleCanvasAudioResources = createSelector( diff --git a/src/state/selectors/config.js b/src/state/selectors/config.js index 63ac28467fec86c6550b5c1e2eb3c7dea7515bbd..d5b99995d2b5458b05dd54e52ee76ed4001b7c09 100644 --- a/src/state/selectors/config.js +++ b/src/state/selectors/config.js @@ -72,6 +72,7 @@ export const getThemeIds = createSelector( ({ themes }) => Object.keys(themes), ); +/* @deprecated */ export const getContainerId = createSelector( [getConfig], ({ id }) => id, diff --git a/src/state/selectors/manifests.js b/src/state/selectors/manifests.js index 48f25eefc77bfb2466c90ceb33435beb3c8e19f2..496d8456216a94e999cce9b7940b7e2fe9d6cf59 100644 --- a/src/state/selectors/manifests.js +++ b/src/state/selectors/manifests.js @@ -1,6 +1,6 @@ import { createSelector } from 'reselect'; import createCachedSelector from 're-reselect'; -import { PropertyValue, Utils } from 'manifesto.js'; +import { PropertyValue, Utils, Resource } from 'manifesto.js'; import getThumbnail from '../../lib/ThumbnailFactory'; import asArray from '../../lib/asArray'; import { getCompanionWindow } from './companionWindows'; @@ -78,17 +78,12 @@ function getProperty(property) { ); } -/** - * Get the logo for a manifest - * @param {object} state - * @param {object} props - * @param {string} props.manifestId - * @param {string} props.windowId - * @return {String|null} - */ -export const getManifestLogo = createSelector( - [getManifestoInstance], - manifest => manifest && manifest.getLogo(), +/** */ +export const getManifestProvider = createSelector( + [ + getProperty('provider'), + ], + (provider) => provider, ); /** @@ -99,7 +94,7 @@ export const getManifestLogo = createSelector( * @param {string} props.windowId * @return {String|null} */ -export const getManifestProvider = createSelector( +export const getManifestProviderName = createSelector( [ getProperty('provider'), getManifestLocale, @@ -109,6 +104,32 @@ export const getManifestProvider = createSelector( && PropertyValue.parse(provider[0].label, locale).getValue(), ); +/** + * Return the IIIF v3 provider logo + * @param {object} state + * @param {object} props + * @return {String|null} + */ +export const getProviderLogo = createSelector( + [getManifestProvider], + (provider) => { + const logo = provider && provider[0] && provider[0].logo && provider[0].logo[0]; + if (!logo) return null; + return getThumbnail(new Resource(logo))?.url; + }, +); + +/** + * Get the logo for a manifest + * @param {object} state + * @param {object} props + * @return {String|null} + */ +export const getManifestLogo = createSelector( + [getManifestoInstance, getProviderLogo], + (manifest, v3logo) => v3logo || (manifest && manifest.getLogo()), +); + /** * Return the IIIF v3 homepage of a manifest or null * @param {object} state @@ -293,12 +314,12 @@ export const getManifestTitle = createSelector( ); /** -* Return manifest description +* Return manifest description (IIIF v2) -- distinct from any description field nested under metadata * @param {object} state * @param {object} props * @param {string} props.manifestId * @param {string} props.windowId -* @return {String} +* @return {String|null} */ export const getManifestDescription = createSelector( [getManifestoInstance], @@ -306,6 +327,23 @@ export const getManifestDescription = createSelector( && manifest.getDescription().getValue(), ); +/** +* Return manifest summary (IIIF v3) +* @param {object} state +* @param {object} props +* @param {string} props.manifestId +* @param {string} props.windowId +* @return {String|null} +*/ +export const getManifestSummary = createSelector( + [ + getProperty('summary'), + getManifestLocale, + ], + (summary, locale) => summary + && PropertyValue.parse(summary, locale).getValue(), +); + /** * Return manifest title * @param {object} state diff --git a/src/state/selectors/workspace.js b/src/state/selectors/workspace.js index 69d8ff62d632dcc9fa1c128c21e3ca3c7bc7a9df..8a7a05dbec8cf113777639638dcad6c8687694ed 100644 --- a/src/state/selectors/workspace.js +++ b/src/state/selectors/workspace.js @@ -26,7 +26,7 @@ export const getWorkspaceType = createSelector( ({ type }) => type, ); -const getFocusedWindowId = createSelector( +export const getFocusedWindowId = createSelector( [getWorkspace], ({ focusedWindowId }) => focusedWindowId, ); diff --git a/src/styles/react-mosaic-component.js b/src/styles/react-mosaic-component.js index 23d46a4fb2c67f5531ab4c4e4a2fc01c3c464110..2f6885ea1208517f1943c303366875083a3d4bfd 100644 --- a/src/styles/react-mosaic-component.js +++ b/src/styles/react-mosaic-component.js @@ -1,285 +1,283 @@ // Generated using jss convert node_modules/react-mosaic-component/react-mosaic-component.css > src/styles/react-mosaic-component.js // .mosaic-blueprint-theme styles have also been removed const globalReactMosaicStyles = { - '@global': { - '.mosaic': { - height: '100%', - width: '100%', - }, - '.mosaic, .mosaic > *': { - boxSizing: 'border-box', - }, - '.mosaic .mosaic-zero-state': { - position: 'absolute', - top: 6, - right: 6, - bottom: 6, - left: 6, - width: 'auto', - height: 'auto', - zIndex: '1', - }, - '.mosaic-root': { - position: 'absolute', - top: 3, - right: 3, - bottom: 3, - left: 3, - }, - '.mosaic-split': { - position: 'absolute', - zIndex: '1', - touchAction: 'none', - }, - '.mosaic-split:hover': { - background: 'black', - }, - '.mosaic-split .mosaic-split-line': { - position: 'absolute', - }, - '.mosaic-split.-row': { - marginLeft: -3, - width: 6, - cursor: 'ew-resize', - }, - '.mosaic-split.-row .mosaic-split-line': { - top: '0', - bottom: '0', - left: 3, - right: 3, - }, - '.mosaic-split.-column': { - marginTop: -3, - height: 6, - cursor: 'ns-resize', - }, - '.mosaic-split.-column .mosaic-split-line': { - top: 3, - bottom: 3, - left: '0', - right: '0', - }, - '.mosaic-tile': { - position: 'absolute', - margin: 3, - }, - '.mosaic-tile > *': { - height: '100%', - width: '100%', - }, - '.mosaic-drop-target': { - position: 'relative', - }, - '.mosaic-drop-target.drop-target-hover .drop-target-container': { - display: 'block', - }, - '.mosaic-drop-target.mosaic > .drop-target-container .drop-target.left': { - right: 'calc(100% - 10px )', - }, - '.mosaic-drop-target.mosaic > .drop-target-container .drop-target.right': { - left: 'calc(100% - 10px )', - }, - '.mosaic-drop-target.mosaic > .drop-target-container .drop-target.bottom': { - top: 'calc(100% - 10px )', - }, - '.mosaic-drop-target.mosaic > .drop-target-container .drop-target.top': { - bottom: 'calc(100% - 10px )', - }, - '.mosaic-drop-target .drop-target-container': { - position: 'absolute', - top: '0', - right: '0', - bottom: '0', - left: '0', - display: 'none', - }, - '.mosaic-drop-target .drop-target-container.-dragging': { - display: 'block', - }, - '.mosaic-drop-target .drop-target-container .drop-target': { - position: 'absolute', - top: '0', - right: '0', - bottom: '0', - left: '0', - background: 'rgba(0, 0, 0, 0.2)', - border: '2px solid black', - opacity: '0', - zIndex: '5', - }, - '.mosaic-drop-target .drop-target-container .drop-target.left': { - right: 'calc(100% - 30% )', - }, - '.mosaic-drop-target .drop-target-container .drop-target.right': { - left: 'calc(100% - 30% )', - }, - '.mosaic-drop-target .drop-target-container .drop-target.bottom': { - top: 'calc(100% - 30% )', - }, - '.mosaic-drop-target .drop-target-container .drop-target.top': { - bottom: 'calc(100% - 30% )', - }, - '.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover': { - opacity: '1', - }, - '.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover.left': { - right: 'calc(100% - 50% )', - }, - '.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover.right': { - left: 'calc(100% - 50% )', - }, - '.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover.bottom': { - top: 'calc(100% - 50% )', - }, - '.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover.top': { - bottom: 'calc(100% - 50% )', - }, - '.mosaic-window, .mosaic-preview': { - position: 'relative', - display: 'flex', - fallbacks: [ - { - display: '-webkit-box', - }, - ], - webkitBoxOrient: 'vertical', - webkitBoxDirection: 'normal', - flexDirection: 'column', - overflow: 'hidden', - boxShadow: '0 0 1px rgba(0, 0, 0, 0.2)', - }, - '.mosaic-window .mosaic-window-toolbar, .mosaic-preview .mosaic-window-toolbar': { - zIndex: '4', - display: 'flex', - fallbacks: [ - { - display: '-webkit-box', - }, - ], - webkitBoxPack: 'justify', - justifyContent: 'space-between', - webkitBoxAlign: 'center', - alignItems: 'center', - flexShrink: '0', - height: 30, - background: 'white', - boxShadow: '0 1px 1px rgba(0, 0, 0, 0.2)', - }, - '.mosaic-window .mosaic-window-toolbar.draggable, .mosaic-preview .mosaic-window-toolbar.draggable': { - cursor: 'move', - }, - '.mosaic-window .mosaic-window-title, .mosaic-preview .mosaic-window-title': { - paddingLeft: 15, - webkitBoxFlex: '1', - flex: '1', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - overflow: 'hidden', - minHeight: 18, - }, - '.mosaic-window .mosaic-window-controls, .mosaic-preview .mosaic-window-controls': { - display: 'flex', - fallbacks: [ - { - display: '-webkit-box', - }, - ], - height: '100%', - }, - '.mosaic-window .mosaic-window-controls .separator, .mosaic-preview .mosaic-window-controls .separator': { - height: 20, - borderLeft: '1px solid black', - margin: '5px 4px', - }, - '.mosaic-window .mosaic-window-body, .mosaic-preview .mosaic-window-body': { - position: 'relative', - webkitBoxFlex: '1', - flex: '1', - height: '0', - background: 'white', - zIndex: '1', - overflow: 'hidden', - }, - '.mosaic-window .mosaic-window-additional-actions-bar, .mosaic-preview .mosaic-window-additional-actions-bar': { - position: 'absolute', - top: 30, - right: '0', - bottom: 'initial', - left: '0', - height: '0', - overflow: 'hidden', - background: 'white', - webkitBoxPack: 'end', - justifyContent: 'flex-end', - display: 'flex', - fallbacks: [ - { - display: '-webkit-box', - }, - ], - zIndex: '3', - }, - '.mosaic-window .mosaic-window-additional-actions-bar .bp3-button, .mosaic-preview .mosaic-window-additional-actions-bar .bp3-button': { - margin: '0', - }, - '.mosaic-window .mosaic-window-additional-actions-bar .bp3-button:after, .mosaic-preview .mosaic-window-additional-actions-bar .bp3-button:after': { - display: 'none', - }, - '.mosaic-window .mosaic-window-body-overlay, .mosaic-preview .mosaic-window-body-overlay': { - position: 'absolute', - top: '0', - right: '0', - bottom: '0', - left: '0', - opacity: '0', - background: 'white', - display: 'none', - zIndex: '2', - }, - '.mosaic-window.additional-controls-open .mosaic-window-additional-actions-bar, .mosaic-preview.additional-controls-open .mosaic-window-additional-actions-bar': { - height: 30, - }, - '.mosaic-window.additional-controls-open .mosaic-window-body-overlay, .mosaic-preview.additional-controls-open .mosaic-window-body-overlay': { - display: 'block', - }, - '.mosaic-window .mosaic-preview, .mosaic-preview .mosaic-preview': { - height: '100%', - width: '100%', - position: 'absolute', - zIndex: '0', - border: '1px solid black', - maxHeight: 400, - }, - '.mosaic-window .mosaic-preview .mosaic-window-body, .mosaic-preview .mosaic-preview .mosaic-window-body': { - display: 'flex', - fallbacks: [ - { - display: '-webkit-box', - }, - ], - webkitBoxOrient: 'vertical', - webkitBoxDirection: 'normal', - flexDirection: 'column', - webkitBoxAlign: 'center', - alignItems: 'center', - webkitBoxPack: 'center', - justifyContent: 'center', - }, - '.mosaic-window .mosaic-preview h4, .mosaic-preview .mosaic-preview h4': { - marginBottom: 10, - }, - '.mosaic:not(.mosaic-blueprint-theme) .mosaic-default-control.close-button:before': { - content: '\'Close\'', - }, - '.mosaic:not(.mosaic-blueprint-theme) .mosaic-default-control.split-button:before': { - content: '\'Split\'', - }, - '.mosaic:not(.mosaic-blueprint-theme) .mosaic-default-control.replace-button:before': { - content: '\'Replace\'', - }, - '.mosaic:not(.mosaic-blueprint-theme) .mosaic-default-control.expand-button:before': { - content: '\'Expand\'', - }, + '.mosaic': { + height: '100%', + width: '100%', + }, + '.mosaic, .mosaic > *': { + boxSizing: 'border-box', + }, + '.mosaic .mosaic-zero-state': { + position: 'absolute', + top: 6, + right: 6, + bottom: 6, + left: 6, + width: 'auto', + height: 'auto', + zIndex: '1', + }, + '.mosaic-root': { + position: 'absolute', + top: 3, + right: 3, + bottom: 3, + left: 3, + }, + '.mosaic-split': { + position: 'absolute', + zIndex: '1', + touchAction: 'none', + }, + '.mosaic-split:hover': { + background: 'black', + }, + '.mosaic-split .mosaic-split-line': { + position: 'absolute', + }, + '.mosaic-split.-row': { + marginLeft: -3, + width: 6, + cursor: 'ew-resize', + }, + '.mosaic-split.-row .mosaic-split-line': { + top: '0', + bottom: '0', + left: 3, + right: 3, + }, + '.mosaic-split.-column': { + marginTop: -3, + height: 6, + cursor: 'ns-resize', + }, + '.mosaic-split.-column .mosaic-split-line': { + top: 3, + bottom: 3, + left: '0', + right: '0', + }, + '.mosaic-tile': { + position: 'absolute', + margin: 3, + }, + '.mosaic-tile > *': { + height: '100%', + width: '100%', + }, + '.mosaic-drop-target': { + position: 'relative', + }, + '.mosaic-drop-target.drop-target-hover .drop-target-container': { + display: 'block', + }, + '.mosaic-drop-target.mosaic > .drop-target-container .drop-target.left': { + right: 'calc(100% - 10px )', + }, + '.mosaic-drop-target.mosaic > .drop-target-container .drop-target.right': { + left: 'calc(100% - 10px )', + }, + '.mosaic-drop-target.mosaic > .drop-target-container .drop-target.bottom': { + top: 'calc(100% - 10px )', + }, + '.mosaic-drop-target.mosaic > .drop-target-container .drop-target.top': { + bottom: 'calc(100% - 10px )', + }, + '.mosaic-drop-target .drop-target-container': { + position: 'absolute', + top: '0', + right: '0', + bottom: '0', + left: '0', + display: 'none', + }, + '.mosaic-drop-target .drop-target-container.-dragging': { + display: 'block', + }, + '.mosaic-drop-target .drop-target-container .drop-target': { + position: 'absolute', + top: '0', + right: '0', + bottom: '0', + left: '0', + background: 'rgba(0, 0, 0, 0.2)', + border: '2px solid black', + opacity: '0', + zIndex: '5', + }, + '.mosaic-drop-target .drop-target-container .drop-target.left': { + right: 'calc(100% - 30% )', + }, + '.mosaic-drop-target .drop-target-container .drop-target.right': { + left: 'calc(100% - 30% )', + }, + '.mosaic-drop-target .drop-target-container .drop-target.bottom': { + top: 'calc(100% - 30% )', + }, + '.mosaic-drop-target .drop-target-container .drop-target.top': { + bottom: 'calc(100% - 30% )', + }, + '.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover': { + opacity: '1', + }, + '.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover.left': { + right: 'calc(100% - 50% )', + }, + '.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover.right': { + left: 'calc(100% - 50% )', + }, + '.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover.bottom': { + top: 'calc(100% - 50% )', + }, + '.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover.top': { + bottom: 'calc(100% - 50% )', + }, + '.mosaic-window, .mosaic-preview': { + position: 'relative', + display: 'flex', + fallbacks: [ + { + display: '-webkit-box', + }, + ], + webkitBoxOrient: 'vertical', + webkitBoxDirection: 'normal', + flexDirection: 'column', + overflow: 'hidden', + boxShadow: '0 0 1px rgba(0, 0, 0, 0.2)', + }, + '.mosaic-window .mosaic-window-toolbar, .mosaic-preview .mosaic-window-toolbar': { + zIndex: '4', + display: 'flex', + fallbacks: [ + { + display: '-webkit-box', + }, + ], + webkitBoxPack: 'justify', + justifyContent: 'space-between', + webkitBoxAlign: 'center', + alignItems: 'center', + flexShrink: '0', + height: 30, + background: 'white', + boxShadow: '0 1px 1px rgba(0, 0, 0, 0.2)', + }, + '.mosaic-window .mosaic-window-toolbar.draggable, .mosaic-preview .mosaic-window-toolbar.draggable': { + cursor: 'move', + }, + '.mosaic-window .mosaic-window-title, .mosaic-preview .mosaic-window-title': { + paddingLeft: 15, + webkitBoxFlex: '1', + flex: '1', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + overflow: 'hidden', + minHeight: 18, + }, + '.mosaic-window .mosaic-window-controls, .mosaic-preview .mosaic-window-controls': { + display: 'flex', + fallbacks: [ + { + display: '-webkit-box', + }, + ], + height: '100%', + }, + '.mosaic-window .mosaic-window-controls .separator, .mosaic-preview .mosaic-window-controls .separator': { + height: 20, + borderLeft: '1px solid black', + margin: '5px 4px', + }, + '.mosaic-window .mosaic-window-body, .mosaic-preview .mosaic-window-body': { + position: 'relative', + webkitBoxFlex: '1', + flex: '1', + height: '0', + background: 'white', + zIndex: '1', + overflow: 'hidden', + }, + '.mosaic-window .mosaic-window-additional-actions-bar, .mosaic-preview .mosaic-window-additional-actions-bar': { + position: 'absolute', + top: 30, + right: '0', + bottom: 'initial', + left: '0', + height: '0', + overflow: 'hidden', + background: 'white', + webkitBoxPack: 'end', + justifyContent: 'flex-end', + display: 'flex', + fallbacks: [ + { + display: '-webkit-box', + }, + ], + zIndex: '3', + }, + '.mosaic-window .mosaic-window-additional-actions-bar .bp3-button, .mosaic-preview .mosaic-window-additional-actions-bar .bp3-button': { + margin: '0', + }, + '.mosaic-window .mosaic-window-additional-actions-bar .bp3-button:after, .mosaic-preview .mosaic-window-additional-actions-bar .bp3-button:after': { + display: 'none', + }, + '.mosaic-window .mosaic-window-body-overlay, .mosaic-preview .mosaic-window-body-overlay': { + position: 'absolute', + top: '0', + right: '0', + bottom: '0', + left: '0', + opacity: '0', + background: 'white', + display: 'none', + zIndex: '2', + }, + '.mosaic-window.additional-controls-open .mosaic-window-additional-actions-bar, .mosaic-preview.additional-controls-open .mosaic-window-additional-actions-bar': { + height: 30, + }, + '.mosaic-window.additional-controls-open .mosaic-window-body-overlay, .mosaic-preview.additional-controls-open .mosaic-window-body-overlay': { + display: 'block', + }, + '.mosaic-window .mosaic-preview, .mosaic-preview .mosaic-preview': { + height: '100%', + width: '100%', + position: 'absolute', + zIndex: '0', + border: '1px solid black', + maxHeight: 400, + }, + '.mosaic-window .mosaic-preview .mosaic-window-body, .mosaic-preview .mosaic-preview .mosaic-window-body': { + display: 'flex', + fallbacks: [ + { + display: '-webkit-box', + }, + ], + webkitBoxOrient: 'vertical', + webkitBoxDirection: 'normal', + flexDirection: 'column', + webkitBoxAlign: 'center', + alignItems: 'center', + webkitBoxPack: 'center', + justifyContent: 'center', + }, + '.mosaic-window .mosaic-preview h4, .mosaic-preview .mosaic-preview h4': { + marginBottom: 10, + }, + '.mosaic:not(.mosaic-blueprint-theme) .mosaic-default-control.close-button:before': { + content: '\'Close\'', + }, + '.mosaic:not(.mosaic-blueprint-theme) .mosaic-default-control.split-button:before': { + content: '\'Split\'', + }, + '.mosaic:not(.mosaic-blueprint-theme) .mosaic-default-control.replace-button:before': { + content: '\'Replace\'', + }, + '.mosaic:not(.mosaic-blueprint-theme) .mosaic-default-control.expand-button:before': { + content: '\'Expand\'', }, }; diff --git a/webpack.config.js b/webpack.config.js index d33c34832911432a1ca12909d7f55ea8e0cb84db..f31af73184ed2f9dd047042324a2a217d45e07ff 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -46,8 +46,6 @@ const baseConfig = mode => ({ ], resolve: { alias: { - // needs shared global state for context to work - 'react-dnd': path.resolve(path.join(__dirname, 'node_modules', 'react-dnd')), 'react/jsx-dev-runtime': 'react/jsx-dev-runtime.js', 'react/jsx-runtime': 'react/jsx-runtime.js', },