Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions __tests__/component/Viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,31 @@ describe('Viewer.open()', () => {
expect(modalProps().show).toBe(true)
})

it.each([
['light', true],
['dark', false],
['default', false],
] as const)('gives the modal a light backdrop only for a %s themed handler', async (theme, lightBackdrop) => {
const handler = makeHandler({ id: theme, tagName: `oca-viewer-${theme}`, theme, enabled: () => true })
const { vm, wrapper, modalProps } = mountViewer([handler])
const f1 = makeFile()
await vm.open([f1], f1)
await wrapper.vm.$nextTick()
expect(modalProps().lightBackdrop).toBe(lightBackdrop)
describe('under the user\'s theme', () => {
afterEach(() => document.body.style.removeProperty('--background-invert-if-dark'))

// The theme the user runs, the one a handler asks for, then whether the
// backdrop is light and whether it takes the theme's colours
it.each([
['light', 'light', true, false],
['light', 'dark', false, false],
['light', 'default', true, true],
['light', undefined, false, false],
['dark', 'light', true, false],
['dark', 'dark', false, false],
['dark', 'default', false, true],
['dark', undefined, false, false],
] as const)('a %s theme gives a %s themed handler a light backdrop: %s, the theme\'s colours: %s', async (userTheme, theme, lightBackdrop, followsTheme) => {
document.body.style.setProperty('--background-invert-if-dark', userTheme === 'dark' ? 'invert(100%)' : 'no')
const id = theme ?? 'unset'
const handler = makeHandler({ id, tagName: `oca-viewer-${id}`, theme, enabled: () => true })
const { vm, wrapper, modalProps } = mountViewer([handler])
const f1 = makeFile()
await vm.open([f1], f1)
await wrapper.vm.$nextTick()
expect(modalProps().lightBackdrop).toBe(lightBackdrop)
expect(wrapper.find('.nc-modal-stub').classes('viewer__modal--default')).toBe(followsTheme)
})
})
})

Expand Down
7 changes: 4 additions & 3 deletions __tests__/component/mountViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import { registerTestHandlers } from '../factories.ts'
* Minimal NcModal stub.
*
* It renders the default slot and the `#actions` slot so we can assert on the
* handler custom-element markup and drive the header actions. The style is
* kept, as the real modal applies it: that is how the viewer makes room for
* the sidebar. Navigation is
* handler custom-element markup and drive the header actions. The style and
* classes are kept, as the real modal applies them: that is how the viewer
* makes room for the sidebar and themes itself. Navigation is
* driven from tests through `findComponent(NcModalStub).vm.$emit('next'|'previous'|'close')`.
* The relevant props (`name`, `show`, `hasNext`, `hasPrevious`, `isComparing`
* related flags) are declared so they can be read back via `.props()`.
Expand All @@ -45,6 +45,7 @@ export const NcModalStub = defineComponent({
<div
v-show="show"
class="nc-modal-stub"
:class="$attrs.class"
:style="$attrs.style"
:data-handler="$attrs['data-handler']"
:data-name="name"
Expand Down
13 changes: 13 additions & 0 deletions e2e/theme.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,17 @@ test.describe('Backdrop', () => {
// What WCAG asks of normal text
expect(contrast).toBeGreaterThanOrEqual(4.5)
})

// The dark palette was set on the whole modal, so it reached into what a
// handler shows: a document in Text came out white on black under a
// light theme, and printed grey
test('leaves the colours of what a handler shows to the theme', async ({ page }) => {
const viewer = new ViewerPage(page)
await viewer.open('photo.jpg')
await viewer.waitForOpen()

const text = (selector: string) => page.locator(selector).first().evaluate((element) => getComputedStyle(element).getPropertyValue('--color-main-text').trim())
expect(await text('.viewer__modal .modal-header')).toBe('#ffffff')
expect(await text('.viewer__modal oca-viewer-image')).not.toBe('#ffffff')
})
})
3 changes: 2 additions & 1 deletion lib/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ export interface IHandler {
onInit?: () => Promise<void>

/**
* Viewer modal theme (one of 'dark', 'light', 'default')
* Viewer modal theme: 'dark' (the default), 'light', or 'default' to
* follow the user's theme
*/
theme?: 'dark' | 'light' | 'default'

Expand Down
60 changes: 44 additions & 16 deletions lib/views/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
:slideshowPaused="slideshowPaused"
:spreadNavigation="true"
:style="{ width: isSidebarShown ? `${sidebarPosition}px` : null }"
:class="{ 'viewer__modal--default': followsTheme }"
class="viewer__modal"
size="full"
@close="close"
Expand Down Expand Up @@ -147,6 +148,7 @@
<!-- Error message -->
<NcEmptyContent
v-else-if="errorString"
class="viewer__error"
:name="errorString"
:description="t('We were unable to display the requested file.')">
<template #icon>
Expand Down Expand Up @@ -353,6 +355,8 @@ const canSwipe = ref(true)
const isFullscreen = ref(false)
const editing = ref(false)
const lightBackdrop = ref(false)
// The handler asked for the user's own theme, backdrop and header included
const followsTheme = ref(false)
const slideshowRunning = ref(false)
// Whether the current handler is playing media, which the slideshow waits for
const playing = ref(false)
Expand Down Expand Up @@ -1157,13 +1161,22 @@ const compare: ViewerAPI['compare'] = async (file1, file2, handlerId) => {
/**
* Handle Viewer opening to determine backdrop style.
*
* The viewer is dark whatever theme the user runs: a photo or a video reads
* better against dark, and the room around it should not compete with it.
* A handler showing something else — a document, say — can ask for a light
* backdrop instead, but nothing follows the user's theme here.
* The viewer is dark unless the handler asks otherwise: a photo or a video
* reads better against dark, and the room around it should not compete with
* it. A handler showing something else, a document say, can ask for a light
* backdrop, or for the user's own theme with 'default'.
*/
function onOpen() {
lightBackdrop.value = (currentHandler.value?.theme ?? 'default') === 'light'
const theme = currentHandler.value?.theme ?? 'dark'
followsTheme.value = theme === 'default'
lightBackdrop.value = theme === 'light' || (followsTheme.value && isThemeLight())
}

/**
* Whether the user runs a light theme, the way the server's own styles tell.
*/
function isThemeLight(): boolean {
return getComputedStyle(document.body).getPropertyValue('--background-invert-if-dark').trim() !== 'invert(100%)'
}

/**
Expand Down Expand Up @@ -1734,17 +1747,32 @@ defineExpose<ViewerAPI>({

<style scoped lang="scss">
.viewer__modal {
// The backdrop is dark whatever theme the server runs, so what sits on
// it has to come from the dark palette too. Without this the header
// inherits the light theme's #222 and lands at 1.3:1 against black,
// which is a title nobody can read. A handler that asked for a light
// backdrop keeps the theme's own colours.
&:not(.modal-mask--light) {
--color-main-text: #ffffff;
--color-text-maxcontrast: #d8d8d8;
--color-main-background: #171717;

color: var(--color-main-text);
// The backdrop is dark whatever theme the server runs, so what the
// viewer itself puts on it has to come from the dark palette too.
// Without this the header inherits the light theme's #222 and lands at
// 1.3:1 against black, which is a title nobody can read. Only the
// viewer's own parts: a handler's content keeps the theme's colours, or
// a document shown by Text turns dark under a light theme. A handler
// that asked for a light backdrop or for the user's theme keeps them
// everywhere.
&:not(.modal-mask--light, .viewer__modal--default) {
:deep(.modal-header),
:deep(.prev),
:deep(.next),
:deep(.modal-container__close),
.viewer__loading,
.viewer__error {
--color-main-text: #ffffff;
--color-text-maxcontrast: #d8d8d8;
--color-main-background: #171717;

color: var(--color-main-text);
}
}

// Over NcModal's translucent black or white backdrop
&--default {
background-color: var(--color-main-background) !important;
}

:deep(.modal-container__content) {
Expand Down
Loading