diff --git a/packages/ui/src/components/va-alert/useAlertStyles.ts b/packages/ui/src/components/va-alert/useAlertStyles.ts
index fbf279e5ae..a7065b70a5 100644
--- a/packages/ui/src/components/va-alert/useAlertStyles.ts
+++ b/packages/ui/src/components/va-alert/useAlertStyles.ts
@@ -1,5 +1,5 @@
import { computed, toRef } from 'vue'
-import { useColors, useTextColor } from '../../composables'
+import { useColors, useCurrentElement, useElementBackground, useElementTextColor, useTextColor } from '../../composables'
type AlertStyleProps = {
modelValue: boolean,
@@ -46,10 +46,12 @@ export const useAlertStyles = (props: AlertStyleProps) => {
}
})
+ const currentColor = useElementTextColor(useElementBackground(useCurrentElement()))
+
const contentStyle = computed(() => {
return {
alignItems: props.center ? 'center' : '',
- color: (props.border || props.outline) ? 'currentColor' : textColorComputed.value,
+ color: (props.border || props.outline) ? currentColor : textColorComputed.value,
}
})
diff --git a/packages/ui/src/components/va-date-picker/components/VaDatePickerHeader/VaDatePickerHeader.vue b/packages/ui/src/components/va-date-picker/components/VaDatePickerHeader/VaDatePickerHeader.vue
index 9dd2289895..af225eea34 100644
--- a/packages/ui/src/components/va-date-picker/components/VaDatePickerHeader/VaDatePickerHeader.vue
+++ b/packages/ui/src/components/va-date-picker/components/VaDatePickerHeader/VaDatePickerHeader.vue
@@ -10,7 +10,7 @@
preset="plain"
size="small"
:color="color"
- :textColor="'currentColor'"
+ :textColor="currentColor"
:aria-label="tp($props.ariaPreviousPeriodLabel)"
round
@click="prev"
@@ -24,7 +24,7 @@
preset="plain"
size="small"
:color="color"
- :textColor="'currentColor'"
+ :textColor="currentColor"
:aria-label="tp($props.ariaSwitchViewLabel)"
@click="switchView"
>
@@ -44,7 +44,7 @@
preset="plain"
size="small"
:color="color"
- :textColor="'currentColor'"
+ :textColor="currentColor"
:aria-label="tp($props.ariaNextPeriodLabel)"
@click="next"
round
@@ -61,7 +61,7 @@ import { useView } from '../../hooks/view'
import { DatePickerView } from '../../types'
import { VaButton } from '../../../va-button'
-import { useTranslation } from '../../../../composables'
+import { useCurrentElement, useElementTextColor, useElementBackground, useTranslation } from '../../../../composables'
export default defineComponent({
name: 'VaDatePickerHeader',
@@ -93,8 +93,11 @@ export default defineComponent({
syncView.value = view
}
+ const currentColor = useElementTextColor(useElementBackground(useCurrentElement()))
+
return {
...useTranslation(),
+ currentColor,
prev,
next,
changeView,
diff --git a/packages/ui/src/composables/UseElementBackgroundDummy.vue b/packages/ui/src/composables/UseElementBackgroundDummy.vue
new file mode 100644
index 0000000000..768e1474a5
--- /dev/null
+++ b/packages/ui/src/composables/UseElementBackgroundDummy.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/packages/ui/src/composables/index.ts b/packages/ui/src/composables/index.ts
index 029a9caa78..07f4800765 100644
--- a/packages/ui/src/composables/index.ts
+++ b/packages/ui/src/composables/index.ts
@@ -72,3 +72,5 @@ export * from './useGlobalConfig'
export * from './usePlacementAliases'
export * from './useDropdownable'
export * from './useTeleported'
+export * from './useElementTextColor'
+export * from './useElementBackground'
diff --git a/packages/ui/src/composables/useElementBackground.stories.ts b/packages/ui/src/composables/useElementBackground.stories.ts
new file mode 100644
index 0000000000..febc1d3a6f
--- /dev/null
+++ b/packages/ui/src/composables/useElementBackground.stories.ts
@@ -0,0 +1,204 @@
+import { defineComponent, computed, ref } from 'vue'
+import { useElementBackground } from './useElementBackground'
+import { useCurrentElement } from './useCurrentElement'
+import { VaButton } from '../components/va-button'
+import UseElementBackgroundDummy from './UseElementBackgroundDummy.vue'
+import { within } from '@storybook/testing-library'
+import { sleep } from '../utils/sleep'
+import { expect } from '@storybook/jest'
+
+export default {
+ title: 'composables/useElementBackground',
+}
+
+export const Default = () => ({
+ components: { VaButton, UseElementBackgroundDummy },
+ data () {
+ return {
+ background: '#000',
+ color: null,
+ }
+ },
+ template: `
+
+
Color detected: {{color}}
+ black
+ white
+`,
+})
+
+Default.play = async ({ canvasElement, step }) => {
+ const canvas = within(canvasElement)
+ const color = canvas.getByTestId('color') as HTMLElement
+ const black = canvas.getByText('black') as HTMLElement
+ const white = canvas.getByText('white') as HTMLElement
+
+ await sleep()
+
+ await step('should match white', async () => {
+ white.click()
+ // Timing before update seems pretty random.
+ // That might be something we want to account for on usage.
+ // 20 ms was a sweet spot for me, 10 was too low.
+ await sleep(20)
+ expect(color.innerText).toBe('rgba255, 255, 255, 1')
+ })
+ await step('should match black', async () => {
+ black.click()
+ await sleep(20)
+ expect(color.innerText).toBe('rgba0, 0, 0, 1')
+ })
+}
+
+export const WithTransition = () => ({
+ data () {
+ return {
+ styles: {
+ background: '#000',
+ color: '#fff',
+ },
+ }
+ },
+
+ setup () {
+ return {
+ color: useElementBackground(useCurrentElement()),
+ }
+ },
+
+ template: `
+
+
+
+
+
Current color: {{ color }}
+
+`,
+})
+
+export const WithOpacity = () => ({
+ data () {
+ return {
+ styles: {
+ background: 'rgba(0, 0, 0, 0.5)',
+ color: '#fff',
+ },
+ }
+ },
+
+ setup () {
+ return {
+ color: useElementBackground(useCurrentElement()),
+ }
+ },
+
+ template: `
+
+
+
+
+
Current color: {{ color }}
+
+`,
+})
+
+export const WithOpacityAndParentWithDynamicBg = () => ({
+ data () {
+ return {
+ styles: {
+ background: 'red',
+ color: '#fff',
+ },
+ }
+ },
+
+ components: {
+ Child: defineComponent({
+ setup () {
+ return {
+ color: useElementBackground(useCurrentElement()),
+ }
+ },
+ template: 'Current color: {{ color }}
',
+ }),
+ },
+
+ template: `
+
+
+
+
+
+
+`,
+})
+
+export const MultipleParentWithDynamicBg = () => ({
+ data () {
+ return {
+ styles: {
+ background: 'red',
+ color: '#fff',
+ },
+ }
+ },
+
+ components: {
+ Child: defineComponent({
+ setup () {
+ return {
+ color: useElementBackground(useCurrentElement()),
+ }
+ },
+ template: 'Current color: {{ color }}
',
+ }),
+ },
+
+ template: `
+
+`,
+})
+
+export const WithOpacityAndWillChange = () => ({
+ data () {
+ return {
+ styles: {
+ background: 'red',
+ color: '#fff',
+ },
+ }
+ },
+
+ components: {
+ Child: defineComponent({
+ setup () {
+ return {
+ color: useElementBackground(useCurrentElement()),
+ }
+ },
+ template: 'Current color: {{ color }}
',
+ }),
+ },
+
+ template: `
+
+
+
+
+
+
+`,
+})
diff --git a/packages/ui/src/composables/useElementBackground.ts b/packages/ui/src/composables/useElementBackground.ts
new file mode 100644
index 0000000000..91285604f4
--- /dev/null
+++ b/packages/ui/src/composables/useElementBackground.ts
@@ -0,0 +1,149 @@
+import { Ref, ref, watchEffect } from 'vue'
+
+/**
+ * This module works with element background
+ * Finding element background is not so easy, because it may be transparent
+ * So we need to find all parent elements with background and apply them
+ * It is not performant, but it is the only way to do it
+ * So there is used ugly code which work fast enough
+ */
+
+/** Value returned from window.getComputedStyle(el).backgroundColor */
+type RGBAColorString = `rgba(${number}, ${number}, ${number}, $${number})`
+/** Parsed value [r, g, b, a] */
+type RGBAColorParsed = [number, number, number, number]
+
+const parseRgba = (rgba: RGBAColorString): RGBAColorParsed => {
+ let values: any[]
+
+ if (rgba.startsWith('rgba')) {
+ values = rgba
+ .substring(5, rgba.length - 1) // Remove 'rgba(' and ')'
+ .split(',')
+ } else {
+ values = rgba
+ .substring(4, rgba.length - 1) // Remove 'rgb(' and ')'
+ .split(',')
+ }
+
+ values[0] = Number(values[0])
+ values[1] = Number(values[1])
+ values[2] = Number(values[2])
+ if (values[3] === undefined) {
+ values[3] = 1
+ } else {
+ values[3] = Number(values[3])
+ }
+
+ return values as RGBAColorParsed
+}
+
+const toHex = (color: RGBAColorParsed): string => {
+ return '#' +
+ (color[0] | 1 << 8).toString(16).slice(1) +
+ (color[1] | 1 << 8).toString(16).slice(1) +
+ (color[2] | 1 << 8).toString(16).slice(1) +
+ (color[3] * 255 | 1 << 8).toString(16).slice(1)
+}
+
+const getParentsWithBackground = (el: HTMLElement): HTMLElement[] => {
+ const parents = []
+
+ let currentEl: HTMLElement | null = el
+ while (currentEl) {
+ if (!(currentEl instanceof HTMLElement) || !currentEl) {
+ return parents
+ }
+
+ const { backgroundColor, willChange } = window.getComputedStyle(currentEl)
+
+ const bgWillChange = willChange.includes('background')
+
+ const parsedColor = parseRgba(backgroundColor as RGBAColorString)
+
+ // In case color is not transparent, we can stop
+ if (parsedColor[3] === 1 && !bgWillChange) {
+ parents.push(currentEl)
+ return parents
+ }
+
+ // If color is not fully transparent we need to add it to parents
+ if (parsedColor[3] !== 0 || bgWillChange) {
+ parents.push(currentEl)
+ }
+
+ currentEl = currentEl.parentElement
+ }
+
+ return parents
+}
+
+// Add fake transition to element to make it trigger transitionend event, add as first class to not break other transitions
+const WATCHER_CLASS = 'va-background-watcher'
+
+const watchElementBackground = (el: HTMLElement, cb: () => void) => {
+ el.className = WATCHER_CLASS + ' ' + el.className
+
+ el.addEventListener('transitionend', (e) => {
+ if (e.target !== el) { return }
+ cb()
+ })
+
+ return () => {
+ el.className = el.className.replace(WATCHER_CLASS, '')
+ el.removeEventListener('transitionend', cb)
+ }
+}
+
+const watchElementsBackground = (els: HTMLElement[], cb: () => void) => {
+ const unwatchers = els.map((el) => watchElementBackground(el, cb))
+
+ return () => {
+ unwatchers.forEach((unwatch) => unwatch())
+ }
+}
+
+/** It is not ideal. browser applies colors in a bit different way */
+const applyColors = (color1: RGBAColorParsed, color2: RGBAColorParsed): RGBAColorParsed => {
+ const weight = color2[3]
+
+ if (weight === 1) { return color2 }
+ if (weight === 0) { return color1 }
+
+ const c1 = Math.round((color1[0]) * (1 - weight) + (color2[0]) * weight)
+ const c2 = Math.round((color1[1]) * (1 - weight) + (color2[1]) * weight)
+ const c3 = Math.round((color1[2]) * (1 - weight) + (color2[2]) * weight)
+
+ return [c1, c2, c3, 1]
+}
+
+const getColorFromElements = (els: HTMLElement[]): RGBAColorParsed => {
+ let currentColor = [0, 0, 0, 0] as RGBAColorParsed
+
+ for (let i = els.length - 1; i >= 0; i--) {
+ currentColor = applyColors(currentColor, parseRgba(window.getComputedStyle(els[i]).backgroundColor as RGBAColorString))
+ }
+
+ return currentColor
+}
+
+export const useElementBackground = (el: Ref) => {
+ const color = ref('#000000')
+ let unWatchAll = () => void 0 as void
+
+ watchEffect(() => {
+ unWatchAll()
+
+ if (el.value) {
+ const parents = getParentsWithBackground(el.value)
+
+ unWatchAll = watchElementsBackground(parents, () => {
+ color.value = toHex(getColorFromElements(parents))
+ })
+
+ color.value = toHex(getColorFromElements(parents))
+ }
+ })
+
+ return color
+}
diff --git a/packages/ui/src/styles/essential.scss b/packages/ui/src/styles/essential.scss
index 9bbb8d7c79..9adaf37278 100644
--- a/packages/ui/src/styles/essential.scss
+++ b/packages/ui/src/styles/essential.scss
@@ -1,2 +1,6 @@
@import './global/theme.scss';
@import './global/css-variables.scss';
+
+.va-background-watcher {
+ transition: 0.01s background-color linear;
+}
\ No newline at end of file
diff --git a/packages/ui/src/styles/index.scss b/packages/ui/src/styles/index.scss
index 9b73f0dfbc..db942f4d48 100644
--- a/packages/ui/src/styles/index.scss
+++ b/packages/ui/src/styles/index.scss
@@ -2,3 +2,4 @@
@import "global/index.scss";
// Mixins, functions and variables
@import "resources/index.scss";
+@import "essential.scss";
\ No newline at end of file