diff --git a/packages/dnb-eufemia-sandbox/stories/components/GlobalStatus.js b/packages/dnb-eufemia-sandbox/stories/components/GlobalStatus.js
index fcd19d2d2b5..10522f3b7e4 100644
--- a/packages/dnb-eufemia-sandbox/stories/components/GlobalStatus.js
+++ b/packages/dnb-eufemia-sandbox/stories/components/GlobalStatus.js
@@ -18,6 +18,7 @@ import {
FormRow,
FormSet,
Autocomplete,
+ DatePicker,
} from '@dnb/eufemia/src/components'
import {
H2,
@@ -63,6 +64,11 @@ export const ComponentAsLabel = () => {
label={}
status={status ? status + '3' : undefined}
/>
+ }
+ show_input
+ status={status ? status + '4' : undefined}
+ />
>
)
diff --git a/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.js b/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.js
index 11cb6bfd776..0e70938dd50 100644
--- a/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.js
+++ b/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.js
@@ -1839,7 +1839,7 @@ class AutocompleteInstance extends React.PureComponent {
icon_size || (size === 'large' ? 'medium' : 'default')
}
size={size}
- status={!opened && status ? status_state : null}
+ status={status ? status_state : null}
status_state={status_state}
type={null}
submit_element={submitButton}
diff --git a/packages/dnb-eufemia/src/components/autocomplete/__tests__/__snapshots__/Autocomplete.test.js.snap b/packages/dnb-eufemia/src/components/autocomplete/__tests__/__snapshots__/Autocomplete.test.js.snap
index 1aeb60e01e1..124cbfa5dc8 100644
--- a/packages/dnb-eufemia/src/components/autocomplete/__tests__/__snapshots__/Autocomplete.test.js.snap
+++ b/packages/dnb-eufemia/src/components/autocomplete/__tests__/__snapshots__/Autocomplete.test.js.snap
@@ -474,7 +474,7 @@ exports[`Autocomplete markup have to match snapshot 1`] = `
size={null}
skeleton="skeleton"
spellCheck="false"
- status={null}
+ status="error"
status_no_animation={null}
status_props={null}
status_state="error"
@@ -506,7 +506,7 @@ exports[`Autocomplete markup have to match snapshot 1`] = `
value="CC cc"
>
@@ -532,13 +532,13 @@ exports[`Autocomplete markup have to match snapshot 1`] = `
label={null}
no_animation={null}
role={null}
- show={null}
+ show={false}
size="default"
skeleton="skeleton"
state="error"
status="error"
stretch={null}
- text={null}
+ text="error"
text_id="autocomplete-id-status"
title={null}
variant={null}
diff --git a/packages/dnb-eufemia/src/components/form-status/FormStatus.js b/packages/dnb-eufemia/src/components/form-status/FormStatus.js
index 11ee51b0778..03df367e7de 100644
--- a/packages/dnb-eufemia/src/components/form-status/FormStatus.js
+++ b/packages/dnb-eufemia/src/components/form-status/FormStatus.js
@@ -179,14 +179,17 @@ export default class FormStatus extends React.PureComponent {
state = { id: null, keepContentInDom: null }
- constructor(props) {
+ constructor(props, context) {
super(props)
// we do not use a random ID here, as we don't need it for now
this.state.id = props.id || makeUniqueId()
this._globalStatus = GlobalStatusProvider.init(
- props.global_status_id || 'main',
+ props?.global_status_id ||
+ context?.FormStatus?.global_status_id ||
+ context?.FormRow?.global_status_id ||
+ 'main',
(provider) => {
// gets called once ready
if (this.props.state === 'error' && this.isReadyToGetVisible()) {
@@ -268,25 +271,30 @@ export default class FormStatus extends React.PureComponent {
// ensure we update the content
this.setState({ keepContentInDom: false })
- const status_id = this.getStatusId()
-
- if (state === 'error' && isTrue(show)) {
- this._globalStatus.update(
- status_id,
- {
- state,
- // status_id,
- item: {
- status_id: this.state.id,
- text,
- status_anchor_label: label,
- status_anchor_url: true,
+ if (state === 'error') {
+ const status_id = this.getStatusId()
+
+ if (isTrue(show)) {
+ this._globalStatus.update(
+ status_id,
+ {
+ state,
+ status_id,
+ item: {
+ item_id: this.state.id,
+ text,
+ status_anchor_label: label,
+ status_anchor_url: true,
+ },
},
- },
- {
- preventRestack: true, // because of the internal "close"
- }
- )
+ {
+ preventRestack: true, // because of the internal "close"
+ }
+ )
+ } else if (!FormStatus.getContent(this.props)) {
+ const status_id = this.getStatusId()
+ this._globalStatus.remove(status_id)
+ }
}
if (this.isReadyToGetVisible()) {
@@ -295,10 +303,6 @@ export default class FormStatus extends React.PureComponent {
this._heightAnim.open()
} else {
this._heightAnim.close()
- if (state === 'error') {
- const status_id = this.getStatusId()
- this._globalStatus.remove(status_id)
- }
}
}
}
diff --git a/packages/dnb-eufemia/src/components/global-status/GlobalStatus.js b/packages/dnb-eufemia/src/components/global-status/GlobalStatus.js
index 9732e82f104..7cb62b1d55c 100644
--- a/packages/dnb-eufemia/src/components/global-status/GlobalStatus.js
+++ b/packages/dnb-eufemia/src/components/global-status/GlobalStatus.js
@@ -280,6 +280,11 @@ export default class GlobalStatus extends React.PureComponent {
this._globalStatus = globalStatus
}
+ let height
+ if (this.state.keepContentVisible) {
+ height = this.anim.adjustFrom()
+ }
+
// force re-render
this.setState({
globalStatus,
@@ -297,7 +302,7 @@ export default class GlobalStatus extends React.PureComponent {
// make sure to show the new status, inc. scroll
if (
(isTrue(this.props.autoclose) &&
- this.hadContent &&
+ this._hadContent &&
!this.hasContent(globalStatus) &&
!isTrue(this.props.show)) ||
(typeof globalStatus.show !== 'undefined' &&
@@ -309,8 +314,13 @@ export default class GlobalStatus extends React.PureComponent {
(typeof globalStatus.show !== 'undefined' &&
isTrue(globalStatus.show))
) {
- this.hadContent = this.hasContent(globalStatus)
- this.setVisible({ delay: 0 })
+ this._hadContent = this.hasContent(globalStatus)
+
+ if (this.state.keepContentVisible) {
+ this.anim.adjustTo(height)
+ } else {
+ this.setVisible({ delay: 0 })
+ }
}
})
@@ -320,8 +330,7 @@ export default class GlobalStatus extends React.PureComponent {
componentDidMount() {
this.anim.setElement(this._shellRef.current)
- const isActive = isTrue(this.props.show)
- if (isActive) {
+ if (isTrue(this.props.show)) {
this.setVisible()
}
}
@@ -353,9 +362,9 @@ export default class GlobalStatus extends React.PureComponent {
globalStatus,
})
}
+
if (prevProps.show !== this.props.show) {
- const isActive = isTrue(this.props.show)
- if (isActive) {
+ if (isTrue(this.props.show)) {
this.setVisible()
} else {
this.setHidden()
@@ -364,7 +373,7 @@ export default class GlobalStatus extends React.PureComponent {
}
hasContent(globalStatus) {
- return globalStatus.items?.length > 0 || globalStatus.text
+ return Boolean(globalStatus.items?.length > 0 || globalStatus.text)
}
correctStatus(state) {
@@ -385,31 +394,10 @@ export default class GlobalStatus extends React.PureComponent {
return // stop here
}
- const { isActive, initialOpened } = this.state
-
- if (isActive === true && initialOpened) {
- if (!this.adjustHeight) {
- this.adjustHeight = this.anim.adjustFrom()
- }
-
- // just because we want to run "adjust" after the content has been set
- this.setState(
- {
- keepContentVisible: true,
- },
- () => {
- this.anim.adjustTo(this.adjustHeight, null, {})
- }
- )
-
- return // stop here
- }
-
const run = () => {
this.setState(
{
isActive: true,
- initialOpened: true,
},
() => {
this.anim.open()
@@ -434,15 +422,17 @@ export default class GlobalStatus extends React.PureComponent {
return // stop here
}
+ this.setState({
+ isClosing: true,
+ })
+
const run = () => {
this.setState(
{
+ isClosing: false,
isActive: false,
- initialOpened: false,
},
- () => {
- this.anim.close()
- }
+ () => this.anim.close()
)
}
@@ -538,7 +528,7 @@ export default class GlobalStatus extends React.PureComponent {
event.persist()
const keyCode = keycode(event)
if (
- (item.status_id &&
+ (item.item_id &&
typeof document !== 'undefined' &&
typeof window !== 'undefined' &&
keyCode === 'space') ||
@@ -548,7 +538,7 @@ export default class GlobalStatus extends React.PureComponent {
event.preventDefault()
try {
// find the element
- const element = document.getElementById(item.status_id)
+ const element = document.getElementById(item.item_id)
if (!element) {
return
@@ -607,9 +597,7 @@ export default class GlobalStatus extends React.PureComponent {
}
const id =
- item.id || item.status_id
- ? `${item.status_id}-${i}`
- : makeUniqueId()
+ item.id || item.item_id ? `${item.item_id}-${i}` : makeUniqueId()
let anchorText = status_anchor_text
@@ -628,7 +616,7 @@ export default class GlobalStatus extends React.PureComponent {
.replace(/[: ]$/g, '')
}
- const useAutolink = item.status_id && isTrue(item.status_anchor_url)
+ const useAutolink = item.item_id && isTrue(item.status_anchor_url)
return (
@@ -642,7 +630,7 @@ export default class GlobalStatus extends React.PureComponent {
aria-describedby={id}
lang={lang}
href={
- useAutolink ? `#${item.status_id}` : item.status_anchor_url
+ useAutolink ? `#${item.item_id}` : item.status_anchor_url
}
onClick={(e) => this.gotoItem(e, item)}
onKeyDown={(e) => this.gotoItem(e, item)}
diff --git a/packages/dnb-eufemia/src/components/global-status/GlobalStatusProvider.js b/packages/dnb-eufemia/src/components/global-status/GlobalStatusProvider.js
index cc48fe6b5c8..f1453c6d741 100644
--- a/packages/dnb-eufemia/src/components/global-status/GlobalStatusProvider.js
+++ b/packages/dnb-eufemia/src/components/global-status/GlobalStatusProvider.js
@@ -55,8 +55,8 @@ class GlobalStatusProvider {
if (typeof item === 'string') {
item = { text: item }
}
- if (!item.status_id) {
- item.status_id =
+ if (!item.item_id) {
+ item.item_id =
status_id && status_id !== 'status-main' // same as defaultProps.status_id
? status_id
: slugify(JSON.stringify(item))
@@ -65,9 +65,9 @@ class GlobalStatusProvider {
}
static combineMessages(stack) {
- const globalStatus = stack.reduce((acc, _cur) => {
+ const globalStatus = stack.reduce((acc, cur) => {
// make a copy, because items are read-only
- const cur = { ..._cur }
+ cur = { ...cur }
if (typeof cur.items === 'string' && cur.items[0] === '[') {
cur.items = JSON.parse(cur.items)
@@ -85,20 +85,20 @@ class GlobalStatusProvider {
// merge items from prev stack into the current
if (cur.items) {
- cur.items = cur.items.reduce((acc, item) => {
+ cur.items = cur.items.reduce((_acc, item) => {
// only a fallback and to make sure we have
item = GlobalStatusProvider.prepareItemWithStatusId(item)
- const foundAtIndex = acc.findIndex(
- ({ status_id }) => status_id === item.status_id
+ const foundAtIndex = _acc.findIndex(
+ ({ item_id }) => item_id === item.item_id
)
if (foundAtIndex > -1) {
- acc[foundAtIndex] = item
+ _acc[foundAtIndex] = item
} else {
- acc.push(item)
+ _acc.push(item)
}
- return acc
+ return _acc
}, acc.items || []) // here we use the items from the prev stack
}
@@ -202,37 +202,33 @@ class GlobalStatusProviderItem {
}
update(status_id, newProps, opts = {}) {
- if (status_id) {
- const item = this.get(status_id)
- if (!item) {
- this.add(newProps)
- }
- }
-
- this.stack = this.stack.map((cur, i, arr) => {
- if (
- !status_id ? i === arr.length - 1 : cur.status_id === status_id
- ) {
- if (!status_id) {
- newProps = { ...newProps }
- delete newProps.status_id
+ const item = this.get(status_id)
+ if (!item) {
+ this.add(newProps, { preventRerender: true })
+ } else {
+ this.stack = this.stack.map((cur, i, arr) => {
+ if (
+ status_id ? cur.status_id === status_id : i === arr.length - 1
+ ) {
+ // if (!status_id) {
+ // // newProps = { ...newProps }
+ // delete newProps.status_id
+ // }
+ return { ...cur, ...newProps }
}
- return { ...cur, ...newProps }
- }
-
- return cur
- })
- if (!opts?.preventRestack) {
- this.restack(status_id)
+ return cur
+ })
}
+ this.restack(status_id)
+
const globalStatus = GlobalStatusProvider.combineMessages(this.stack)
if (!opts?.preventRerender) {
this.forceRerender(globalStatus, null, {
buffer_delay:
- newProps?.buffer_delay > -1 ? newProps.buffer_delay : 0,
+ newProps?.buffer_delay > -1 ? newProps.buffer_delay : 1,
})
}
}
@@ -258,7 +254,7 @@ class GlobalStatusProviderItem {
if (!opts?.preventRerender) {
this.forceRerender(globalStatus, null, {
- buffer_delay: opts?.buffer_delay > -1 ? opts.buffer_delay : 10,
+ buffer_delay: opts?.buffer_delay > -1 ? opts.buffer_delay : 1,
})
}
}
diff --git a/packages/dnb-eufemia/src/components/global-status/__tests__/GlobalStatus.test.js b/packages/dnb-eufemia/src/components/global-status/__tests__/GlobalStatus.test.js
index 3ebf86d6a46..3d6367e59a9 100644
--- a/packages/dnb-eufemia/src/components/global-status/__tests__/GlobalStatus.test.js
+++ b/packages/dnb-eufemia/src/components/global-status/__tests__/GlobalStatus.test.js
@@ -12,7 +12,9 @@ import {
loadScss,
} from '../../../core/jest/jestSetup'
import Component from '../GlobalStatus'
+import FormSet from '../../form-set/FormSet'
import Switch from '../../switch/Switch'
+import Autocomplete from '../../autocomplete/Autocomplete'
const id = 'main'
const status_id = null
@@ -53,9 +55,8 @@ const props = {
}
describe('GlobalStatus component', () => {
- const Comp = mount()
-
it('has to have a text value as defined in the prop', () => {
+ const Comp = mount()
expect(
Comp.find('div.dnb-global-status__message')
.find('.dnb-p')
@@ -65,6 +66,7 @@ describe('GlobalStatus component', () => {
})
it('has to have list items as defined in the prop', () => {
+ const Comp = mount()
expect(Comp.find('.dnb-ul').text()).toBe(
props.items.map(({ text }) => text).join('')
)
@@ -241,11 +243,6 @@ describe('GlobalStatus component', () => {
Comp.find('div.dnb-global-status__message p.dnb-p').at(0).text()
).toBe(startupText)
expect(Comp.exists('div.dnb-global-status__message')).toBe(true)
- expect(
- Comp.find('div.dnb-global-status__shell')
- .instance()
- .getAttribute('style')
- ).toBe('height: auto;')
mount(
{
).toBe('height: 0px; visibility: hidden;')
})
+ it('have to handle delayed interactions ', async () => {
+ const FormField1 = () => {
+ const [status, setStatus] = React.useState()
+ return (
+ {
+ setStatus(checked ? 'error-message-1' : null)
+ }}
+ />
+ )
+ }
+
+ const FormField2 = () => {
+ const [status, setStatus] = React.useState()
+ return (
+ {
+ setStatus(checked ? 'error-message-2' : null)
+ }}
+ />
+ )
+ }
+
+ const FormField3 = () => {
+ const [status, setStatus] = React.useState()
+ return (
+ {
+ setStatus('error-message-3')
+ }}
+ on_blur={() => {
+ setStatus(null)
+ }}
+ />
+ )
+ }
+
+ const Comp = mount(
+ <>
+
+
+
+
+
+
+ >
+ )
+
+ await wait(1)
+ Comp.find('input#switch-1').simulate('change')
+
+ await wait(1)
+ Comp.find('input#switch-2').simulate('change')
+
+ await wait(1)
+ Comp.find('input#autocomplete-3').simulate('focus')
+
+ // FormStatus content
+ expect(Comp.find('.dnb-form-status__text').at(0).text()).toBe(
+ 'error-message-1'
+ )
+ expect(Comp.find('.dnb-form-status__text').at(1).text()).toBe(
+ 'error-message-2'
+ )
+ expect(
+ Comp.find('.dnb-autocomplete')
+ .at(0)
+ .find('.dnb-form-status__text')
+ .text()
+ ).toBe('error-message-3')
+
+ await refresh(Comp)
+
+ // GlobalStatus content
+ expect(Comp.find('.dnb-global-status__message p').at(0).text()).toBe(
+ 'error-message-1'
+ )
+ expect(Comp.find('.dnb-global-status__message p').at(1).text()).toBe(
+ 'error-message-2'
+ )
+ expect(Comp.find('.dnb-global-status__message p').at(2).text()).toBe(
+ 'error-message-3'
+ )
+
+ await wait(1)
+ Comp.find('input#switch-1').simulate('change')
+
+ await wait(1)
+ Comp.find('input#switch-2').simulate('change')
+
+ await wait(1)
+ Comp.find('input#autocomplete-3').simulate('blur')
+
+ expect(Comp.exists('.dnb-form-status__text')).toBe(false)
+
+ await refresh(Comp)
+
+ expect(Comp.exists('.dnb-global-status__message p')).toBe(false)
+ expect(Comp.exists('.dnb-form-status__text')).toBe(false)
+ const inst = Comp.find('div.dnb-global-status__shell').instance()
+ expect(inst.innerHTML).toBe('')
+ expect(inst.getAttribute('style')).toBe(
+ 'height: 0px; visibility: hidden;'
+ )
+ })
+
+ it('have to scroll to GlobalStatus ', async () => {
+ const scrollTo = jest.fn()
+ jest.spyOn(window, 'scrollTo').mockImplementation(scrollTo)
+ const offsetTop = 1000
+
+ const ToggleStatus = () => {
+ const [status, setStatus] = React.useState(null)
+
+ return (
+ {
+ setStatus(checked ? 'error-message' : null)
+ }}
+ />
+ )
+ }
+ const Comp = mount(
+ <>
+
+
+ >
+ )
+
+ // Open
+ Comp.find('input#switch').simulate('change')
+ await refresh(Comp)
+
+ expect(scrollTo).toBeCalledTimes(1)
+ expect(scrollTo).toHaveBeenCalledWith({
+ behavior: 'smooth',
+ top: 0,
+ })
+
+ jest
+ .spyOn(
+ Comp.find('.dnb-global-status__wrapper').instance(),
+ 'offsetTop',
+ 'get'
+ )
+ .mockImplementation(() => offsetTop)
+
+ // Close
+ Comp.find('input#switch').simulate('change')
+ await refresh(Comp)
+
+ expect(scrollTo).toBeCalledTimes(1)
+
+ // Open
+ Comp.find('input#switch').simulate('change')
+ await refresh(Comp)
+
+ expect(scrollTo).toBeCalledTimes(2)
+ expect(scrollTo).toHaveBeenCalledWith({
+ behavior: 'smooth',
+ top: offsetTop,
+ })
+ })
+
+ it('have to close when esc key is pressed ', async () => {
+ const on_close = jest.fn()
+ const on_hide = jest.fn()
+
+ const ToggleStatus = () => {
+ const [status, setStatus] = React.useState(null)
+
+ return (
+ {
+ setStatus(checked ? 'error-message' : null)
+ }}
+ />
+ )
+ }
+ const Comp = mount(
+ <>
+
+
+ >
+ )
+
+ // Open
+ Comp.find('input#switch').simulate('change')
+ await refresh(Comp)
+
+ expect(on_close).toBeCalledTimes(0)
+
+ // Close with key
+ keydown(Comp, 27) // esc
+
+ expect(on_hide).toBeCalledTimes(1)
+ expect(on_close).toBeCalledTimes(1)
+ })
+
+ it('have to have height of auto value', async () => {
+ const ToggleStatus = () => {
+ const [status, setStatus] = React.useState(null)
+
+ return (
+ {
+ setStatus(checked ? 'error-message' : null)
+ }}
+ />
+ )
+ }
+ const Comp = mount(
+ <>
+
+
+ >
+ )
+
+ Comp.find('input#switch').simulate('change')
+ await refresh(Comp)
+
+ expect(
+ Comp.find('div.dnb-global-status__shell')
+ .instance()
+ .getAttribute('style')
+ ).toBe('height: auto;')
+ })
+
it('have to be hidden after all messages are removed ', async () => {
const ToggleStatus = () => {
const [status, setStatus] = React.useState(null)
@@ -338,23 +596,19 @@ describe('GlobalStatus component', () => {
)
Comp.find('input#switch').simulate('change')
+ await refresh(Comp)
expect(Comp.find('.dnb-form-status__text').text()).toBe(
'error-message'
)
- expect(Comp.find('.dnb-global-status__message p').at(0).text()).toBe(
+
+ expect(Comp.exists('.dnb-global-status__content')).toBe(true)
+ expect(Comp.find('.dnb-global-status__message p').text()).toBe(
'error-message'
)
- expect(
- Comp.find('div.dnb-global-status__shell')
- .instance()
- .getAttribute('style')
- ).toBe('height: auto;')
-
Comp.find('input#switch').simulate('change')
-
- await wait(10)
+ await refresh(Comp)
expect(Comp.exists('.dnb-form-status__text')).toBe(false)
const inst = Comp.find('div.dnb-global-status__shell').instance()
@@ -400,6 +654,8 @@ describe('GlobalStatus component', () => {
Comp.find('input#switch').simulate('change')
+ await refresh(Comp)
+
expect(Comp.find('.dnb-global-status__message p').at(0).text()).toBe(
'error-message'
)
@@ -558,6 +814,7 @@ describe('GlobalStatus component', () => {
})
it('should validate with ARIA rules', async () => {
+ const Comp = mount()
expect(await axeComponent(Comp)).toHaveNoViolations()
})
})
@@ -606,3 +863,16 @@ describe('GlobalStatus scss', () => {
})
const wait = (t) => new Promise((r) => setTimeout(r, t))
+
+const refresh = async (Comp) => {
+ await wait(1)
+ Comp.update()
+}
+
+const keydown = (Comp, keyCode) => {
+ document.dispatchEvent(new KeyboardEvent('keydown', { keyCode }))
+
+ Comp.find('.dnb-global-status__wrapper').simulate('keydown', {
+ keyCode,
+ })
+}
diff --git a/packages/dnb-eufemia/src/components/global-status/__tests__/__snapshots__/GlobalStatus.test.js.snap b/packages/dnb-eufemia/src/components/global-status/__tests__/__snapshots__/GlobalStatus.test.js.snap
index 1fa9d4c3a59..ef90c83d4a6 100644
--- a/packages/dnb-eufemia/src/components/global-status/__tests__/__snapshots__/GlobalStatus.test.js.snap
+++ b/packages/dnb-eufemia/src/components/global-status/__tests__/__snapshots__/GlobalStatus.test.js.snap
@@ -768,12 +768,12 @@ exports[`GlobalStatus snapshot have to match component snapshot 1`] = `
Array [
Object {
"id": "id-1",
- "status_id": "idid-1textitem-1",
+ "item_id": "idid-1textitem-1",
"text": "item #1",
},
Object {
"id": "id-2",
- "status_id": "idid-2textitem-2",
+ "item_id": "idid-2textitem-2",
"text": "item #2",
},
]
@@ -1467,14 +1467,14 @@ Array [
>
error-message