diff --git a/.changeset/cuddly-horses-teach.md b/.changeset/cuddly-horses-teach.md new file mode 100644 index 0000000000..9f23b05d31 --- /dev/null +++ b/.changeset/cuddly-horses-teach.md @@ -0,0 +1,5 @@ +--- +'@baloise/ds-core': minor +--- + +**dropdown**: add new component to replace bal-select diff --git a/.changeset/modern-bags-worry.md b/.changeset/modern-bags-worry.md new file mode 100644 index 0000000000..ed1cab50ca --- /dev/null +++ b/.changeset/modern-bags-worry.md @@ -0,0 +1,5 @@ +--- +'@baloise/ds-core': minor +--- + +**option-list**: new child component of drop-down components. Option list component will be used by drop-down, combobox and autocomplete. diff --git a/.changeset/ten-teachers-appear.md b/.changeset/ten-teachers-appear.md new file mode 100644 index 0000000000..362cefa739 --- /dev/null +++ b/.changeset/ten-teachers-appear.md @@ -0,0 +1,5 @@ +--- +'@baloise/ds-core': minor +--- + +**option**: new child component of option-list. Option list component will be used by drop-down, combobox and autocomplete. diff --git a/docs/stories/components/bal-dropdown/bal-dropdown.mdx b/docs/stories/components/bal-dropdown/bal-dropdown.mdx new file mode 100644 index 0000000000..053265c21a --- /dev/null +++ b/docs/stories/components/bal-dropdown/bal-dropdown.mdx @@ -0,0 +1,119 @@ +import { Canvas, Meta, Markdown } from '@storybook/blocks' +import { Banner, Lead, PlaygroundBar, StoryHeading, Footer } from '../../../.storybook/blocks' +import * as DropDownStories from './bal-dropdown.stories' + + + + + + + +**Drop-Down** components are used to gather user-provided information from a range of options. + +The `bal-dropdown` component serves as a alternative to a traditional ` + +`, + ), +}) diff --git a/docs/stories/components/bal-dropdown/testing.md b/docs/stories/components/bal-dropdown/testing.md new file mode 100644 index 0000000000..7e2fe1906e --- /dev/null +++ b/docs/stories/components/bal-dropdown/testing.md @@ -0,0 +1,32 @@ +## Testing + +The Baloise Design System provides a collection of custom cypress commands for the components. Moreover, some basic cypress commands like `should` or `click` have been overridden to work with the components. + +Go to testing guide + + + +```ts +describe('Dropdown', () => { + it('should ...', () => { + cy.getByPlaceholder('Pick a color').click() + cy.getByRole('option', { name: 'Red' }).click() + + cy.getByPlaceholder('Pick a color').should('be.disabled') + cy.getByPlaceholder('Pick a color').should('have.value', 'Red') + }) +}) +``` + + + + +### Selectors + +| Selector | Element | +| ------------------ | ------------------------------------ | +| `dropdown.input` | Native input element. | +| `dropdown.options` | Select option. | +| `dropdown.trigger` | Trigger to open and close the popup. | +| `dropdown.chips` | Multi select tag . | + diff --git a/docs/stories/components/bal-dropdown/theming.md b/docs/stories/components/bal-dropdown/theming.md new file mode 100644 index 0000000000..901672d6a4 --- /dev/null +++ b/docs/stories/components/bal-dropdown/theming.md @@ -0,0 +1,46 @@ +## Theming + +The component can be customization by changing the CSS variables. + +Go to theming guide + + + + + + + +### Variables​ + +| Variable | +| ---------------------------------------------------------------------- | +| `--bal-dropdown-control-background` | +| `--bal-dropdown-control-background-hover` | +| `--bal-dropdown-control-background-invalid` | +| `--bal-dropdown-control-background-disabled` | +| `--bal-dropdown-control-input-background` | +| `--bal-dropdown-control-native-input-background` | +| `--bal-dropdown-control-native-input-background-hover` | +| `--bal-dropdown-control-input-inverted-footer-background` | +| `--bal-dropdown-control-input-inverted-footer-background-hover` | +| `--bal-dropdown-control-input-multiple-background` | +| `--bal-dropdown-control-input-multiple-background-read-only-selection` | +| `--bal-dropdown-control-input-option-background` | +| `--bal-dropdown-control-input-option-background-selected` | +| `--bal-dropdown-control-input-option-background-focused` | +| `--bal-dropdown-control-input-option-background-hover` | +| `--bal-dropdown-control-border-radius` | +| `--bal-dropdown-popover-border-color` | +| `--bal-dropdown-control-border-color` | +| `--bal-dropdown-control-border-color-focused` | +| `--bal-dropdown-control-border-color-hover` | +| `--bal-dropdown-control-border-color-invalid` | +| `--bal-dropdown-control-border-color-disabled` | +| `--bal-dropdown-control-border-color-focus-within` | +| `--bal-dropdown-option-border-top-color` | +| `--bal-dropdown-popover-empty-text-color` | +| `--bal-dropdown-control-text-color` | +| `--bal-dropdown-control-text-color-focused` | +| `--bal-dropdown-input-text-color-disabled` | +| `--bal-dropdown-control-inverted-footer-native-input-text-color` | +| `--bal-dropdown-option-content-label-text-color` | diff --git a/docs/stories/components/bal-option-list/testing.md b/docs/stories/components/bal-option-list/testing.md new file mode 100644 index 0000000000..98d460be9e --- /dev/null +++ b/docs/stories/components/bal-option-list/testing.md @@ -0,0 +1,12 @@ +## Testing + +The Baloise Design System provides a collection of custom cypress commands for the components. Moreover, some basic cypress commands like `should` or `click` have been overridden to work with the components. + +Go to testing guide + + + + + + + diff --git a/docs/stories/components/bal-option-list/theming.md b/docs/stories/components/bal-option-list/theming.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/stories/components/bal-option/testing.md b/docs/stories/components/bal-option/testing.md new file mode 100644 index 0000000000..98d460be9e --- /dev/null +++ b/docs/stories/components/bal-option/testing.md @@ -0,0 +1,12 @@ +## Testing + +The Baloise Design System provides a collection of custom cypress commands for the components. Moreover, some basic cypress commands like `should` or `click` have been overridden to work with the components. + +Go to testing guide + + + + + + + diff --git a/docs/stories/components/bal-option/theming.md b/docs/stories/components/bal-option/theming.md new file mode 100644 index 0000000000..a3fa61a158 --- /dev/null +++ b/docs/stories/components/bal-option/theming.md @@ -0,0 +1,32 @@ +## Theming + +The component can be customization by changing the CSS variables. + +Go to theming guide + + + + + + + +### Variables​ + +| Variable | +| ------------------------------------------ | +| `--bal-option-padding-x` | +| `--bal-option-min-height` | +| `--bal-option-background` | +| `--bal-option-background-hovered` | +| `--bal-option-background-selected` | +| `--bal-option-background-selected-hovered` | +| `--bal-option-background-invalid` | +| `--bal-option-background-invalid-hovered` | +| `--bal-option-border-width` | +| `--bal-option-border-color` | +| `--bal-option-font-family` | +| `--bal-option-font-weight` | +| `--bal-option-line-height` | +| `--bal-option-text-hovered` | +| `--bal-option-text-pressed` | +| `--bal-option-text-disabled` | diff --git a/e2e/cypress/component/bal-dropdown.cy.ts b/e2e/cypress/component/bal-dropdown.cy.ts new file mode 100644 index 0000000000..7594a28e25 --- /dev/null +++ b/e2e/cypress/component/bal-dropdown.cy.ts @@ -0,0 +1,484 @@ +import { newBalOption } from '../../generated/components' +import { Components } from '../support/utils' + +describe('bal-dropdown', () => { + let onBalChangeSpy: Cypress.Agent + + let options = [] + let events = { + balChange: onBalChangeSpy, + } + + const template = ` + Green + Red + Yellow + Purple +` + + beforeEach(() => { + options = [ + newBalOption({ value: 'vGreen', label: 'Green' }), + newBalOption({ value: 'vRed', label: 'Red' }), + newBalOption({ value: 'vPurple', label: 'Purple' }), + newBalOption({ value: 'vYellow', label: 'Yellow' }), + ] + onBalChangeSpy = cy.spy().as('balChange') + events = { + balChange: onBalChangeSpy, + } + }) + + context('template options', () => { + it('should select option and emit change event', () => { + cy.mount(template, { + props: { + placeholder: 'Pick a color', + }, + events, + }) + + cy.getByPlaceholder('Pick a color').click() + cy.getByRole('option', { name: 'Red' }).click() + cy.get('@balChange').should('have.been.calledOnce') + cy.get('@balChange').shouldHaveEventDetail('vRed') + cy.getByPlaceholder('Pick a color').should('have.value', 'Red') + }) + + it('should select multiple options and emit 2 change events', () => { + cy.mount(template, { + props: { + placeholder: 'Pick a color', + multiple: true, + }, + events, + }) + + cy.getByPlaceholder('Pick a color').click() + cy.getByRole('option', { name: 'Red' }).click() + cy.getByRole('option', { name: 'Purple' }).click() + cy.get('@balChange').should('have.been.calledTwice') + cy.get('@balChange').shouldHaveEventDetail(['vRed']) + cy.get('@balChange').shouldHaveEventDetail(['vRed', 'vPurple'], 1) + cy.getByPlaceholder('Pick a color').should('have.value', ['Red', 'Purple']) + }) + + it('should not select because component is disabled', () => { + cy.mount(template, { + props: { + placeholder: 'Pick a color', + disabled: true, + }, + events, + }) + + cy.getByPlaceholder('Pick a color').click({ force: true }) + cy.getByRole('option', { name: 'Red' }).click({ force: true }) + cy.get('@balChange').should('not.have.been.called') + cy.getByPlaceholder('Pick a color').should('have.value', '') + }) + + it('should clear values and emit a change event', () => { + cy.mount(template, { + props: { + placeholder: 'Pick a color', + clearable: true, + value: ['vYellow'], + }, + events, + }) + + cy.getByRole('button', { name: 'Löschen' }).click() + cy.get('@balChange').should('have.been.calledOnce') + cy.get('@balChange').shouldHaveEventDetail(null) + cy.getByPlaceholder('Pick a color').should('have.value', '') + }) + }) + + context('prop options', () => { + it('should select option and emit change event', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + options, + }, + events, + }) + + cy.getByPlaceholder('Pick a color').click() + cy.getByRole('option', { name: 'Red' }).click() + cy.get('@balChange').should('have.been.calledOnce') + cy.get('@balChange').shouldHaveEventDetail('vRed') + cy.getByPlaceholder('Pick a color').should('have.value', 'Red') + }) + + it('should select multiple options and emit 2 change events', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + multiple: true, + options, + }, + events, + }) + + cy.getByPlaceholder('Pick a color').click() + cy.getByRole('option', { name: 'Red' }).click() + cy.getByRole('option', { name: 'Purple' }).click() + cy.get('@balChange').should('have.been.calledTwice') + cy.get('@balChange').shouldHaveEventDetail(['vRed']) + cy.get('@balChange').shouldHaveEventDetail(['vRed', 'vPurple'], 1) + cy.getByPlaceholder('Pick a color').should('have.value', ['Red', 'Purple']) + }) + + it('should not select because component is disabled', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + disabled: true, + options, + }, + events, + }) + + cy.getByPlaceholder('Pick a color').click({ force: true }) + cy.getByRole('option', { name: 'Red' }).click({ force: true }) + cy.get('@balChange').should('not.have.been.called') + cy.getByPlaceholder('Pick a color').should('have.value', '') + }) + + it('should clear values and emit a change event', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + clearable: true, + options, + value: ['vYellow'], + }, + events, + }) + + cy.getByRole('button', { name: 'Löschen' }).click() + cy.get('@balChange').should('have.been.calledOnce') + cy.get('@balChange').shouldHaveEventDetail(null) + cy.getByPlaceholder('Pick a color').should('have.value', '') + }) + }) + + context('required', () => { + it('should only once send a change event', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + required: true, + value: [], + options, + }, + events, + }) + + cy.getByPlaceholder('Pick a color').should('have.value', '').click() + cy.getByRole('option', { name: 'Red' }).click() + cy.getByPlaceholder('Pick a color').should('have.value', 'Red').click() + cy.getByRole('option', { name: 'Red' }).click() + + cy.get('@balChange').should('have.been.calledOnce') + cy.get('@balChange').shouldHaveEventDetail('vRed') + }) + + it('should not be able to deselect', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + required: true, + value: ['vRed'], + options, + }, + events, + }) + + cy.getByPlaceholder('Pick a color').should('have.value', 'Red').click() + cy.getByRole('option', { name: 'Red' }).click() + cy.getByPlaceholder('Pick a color').should('have.value', 'Red') + cy.get('@balChange').should('not.have.been.called') + }) + }) + + context('invalid', () => { + it('should be valid', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + invalid: false, + value: [], + options, + }, + events, + }) + + cy.getByPlaceholder('Pick a color').shouldBeValid() + }) + + it('should be invalid', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + invalid: true, + value: [], + options, + }, + events, + }) + + cy.getByPlaceholder('Pick a color').shouldBeInvalid() + }) + }) + + context('multiple + chips', () => { + it('should remove option by clicking the chip', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + chips: true, + multiple: true, + value: ['vRed', 'vPurple'], + options, + }, + events, + }) + + cy.getByRole('button', { name: 'Schliessen' }).first().click({ force: true }) + cy.get('@balChange').should('have.been.calledOnce') + cy.get('@balChange').shouldHaveEventDetail(['vPurple']) + cy.getByPlaceholder('Pick a color').should('have.value', ['Purple']) + }) + }) + + context('key combos', () => { + it('should use arrow key to select option and emit change event', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + options, + }, + events, + }) + + cy.getByPlaceholder('Pick a color').click().type('{downArrow}').type('{enter}') + + cy.get('@balChange').should('have.been.calledOnce') + cy.get('@balChange').shouldHaveEventDetail('vRed') + cy.getByPlaceholder('Pick a color').should('have.value', 'Red') + }) + + it('should use arrow key up and down to select multiple options and emit change event', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + multiple: true, + options, + }, + events, + }) + + cy.getByPlaceholder('Pick a color') + .click() + .type('{downArrow}') + .type('{downArrow}') + .type('{downArrow}') + .type('{enter}') + .type('{upArrow}') + .type('{enter}') + + cy.get('@balChange').should('have.been.calledTwice') + cy.get('@balChange').shouldHaveEventDetail(['vYellow']) + cy.get('@balChange').shouldHaveEventDetail(['vPurple', 'vYellow'], 1) + cy.getByPlaceholder('Pick a color').should('have.value', ['Purple', 'Yellow']) + }) + + it('should use focus by label to select option and emit change event', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + options, + }, + events, + }) + + cy.getByPlaceholder('Pick a color').click().type('{Y}').wait(200).type('{enter}') + + cy.get('@balChange').should('have.been.calledOnce') + cy.get('@balChange').shouldHaveEventDetail('vYellow') + cy.getByPlaceholder('Pick a color').should('have.value', 'Yellow') + }) + + it('should use focus by label to select option and emit change event without open it', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + options, + }, + events, + }) + + cy.getByPlaceholder('Pick a color').focus().type('{Y}').wait(200).blur() + + cy.get('@balChange').should('have.been.calledOnce') + cy.get('@balChange').shouldHaveEventDetail('vYellow') + cy.getByPlaceholder('Pick a color').should('have.value', 'Yellow') + }) + }) + + context('value + no options', () => { + it('should show an empty dropdown since there is no option to match the value', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + value: 'vRed', + }, + events, + }) + + cy.get('.bal-dropdown__root__content').should('be.empty') + cy.getByPlaceholder('Pick a color').should('have.value', '') + }) + + it('should update dropdown after option update', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + value: 'vRed', + }, + events, + }) + + cy.wait(200) + .get('bal-dropdown') + .then($el => { + $el.get(0).options = options + }) + + cy.get('.bal-dropdown__root__content').contains('Red') + cy.getByPlaceholder('Pick a color').should('have.value', 'Red') + }) + + it('should work with dynamic chaning the options and values', () => { + cy.mount(``, { + props: { + placeholder: 'Pick a color', + options, + value: 'vRed', + }, + events, + }) + + cy.wait(200) + .get('bal-dropdown') + .then($el => { + $el.get(0).options = [ + newBalOption({ value: 'vApple', label: 'Apple' }), + newBalOption({ value: 'vOrange', label: 'Orange' }), + newBalOption({ value: 'vBanana', label: 'Banana' }), + ] as any + }) + + cy.get('.bal-dropdown__root__content').should('be.empty') + cy.getByPlaceholder('Pick a color').should('have.value', '') + + cy.getByPlaceholder('Pick a color').click({ force: true }) + cy.getByRole('option', { name: 'Banana' }).click({ force: true }) + + cy.get('.bal-dropdown__root__content').contains('Banana') + cy.getByPlaceholder('Pick a color').should('have.value', 'Banana') + cy.get('@balChange').should('have.been.calledOnce') + cy.get('@balChange').shouldHaveEventDetail('vBanana') + }) + }) + + context('a11y field label', () => { + it('should pick a option with label linking', () => { + cy.mount( + ` + Color + + + + `, + { + props: { + placeholder: 'Pick a color', + options, + }, + events, + }, + ) + + cy.getByLabelText('Color').click() + cy.getByRole('option', { name: 'Green' }).click() + cy.get('@balChange').should('have.been.calledOnce') + cy.get('@balChange').shouldHaveEventDetail('vGreen') + cy.getByPlaceholder('Pick a color').should('have.value', 'Green') + }) + + it('should not select option since it is disabled', () => { + cy.mount( + ` + Color + + + + `, + { + props: { + placeholder: 'Pick a color', + options, + }, + events, + }, + ) + + cy.getByLabelText('Color').click({ force: true }) + cy.getByRole('option', { name: 'Green' }).click({ force: true }) + cy.get('@balChange').should('not.have.been.called') + cy.getByPlaceholder('Pick a color').should('have.value', '') + }) + }) + + context('form reset', () => { + it('should remove option by clicking the chip', () => { + cy.mount( + ` +
+ + + + Country + + + Switzerland + Germany + Italy + + + + + + + +
`, + { + props: { + placeholder: 'Pick your country', + value: 'Germany', + }, + events, + }, + ) + + cy.getByLabelText('Country').click() + cy.getByRole('option', { name: 'Italy' }).click() + cy.getByRole('input', { name: 'Reset' }).click() + cy.getByLabelText('Country').should('have.value', 'Germany') + }) + }) +}) diff --git a/e2e/cypress/component/bal-option-list.cy.ts b/e2e/cypress/component/bal-option-list.cy.ts new file mode 100644 index 0000000000..070b915e73 --- /dev/null +++ b/e2e/cypress/component/bal-option-list.cy.ts @@ -0,0 +1,24 @@ +import { Components } from '../support/utils' + +describe('bal-option-list', () => { + it('should select an option and emit an event', () => { + const onBalOptionChangeSpy = cy.spy().as('balOptionChange') + cy.mount( + ` + Green + Red + Yellow + Purple + `, + { + events: { + balOptionChange: onBalOptionChangeSpy, + }, + }, + ) + + cy.getByRole('option', { name: 'Red' }).click() + cy.get('@balOptionChange').should('have.been.calledOnce') + cy.get('@balOptionChange').shouldHaveEventDetail({ label: 'Red', value: 'vRed', selected: true }) + }) +}) diff --git a/e2e/cypress/e2e/a11y/bal-dropdown.a11y.cy.ts b/e2e/cypress/e2e/a11y/bal-dropdown.a11y.cy.ts new file mode 100644 index 0000000000..3528fabea6 --- /dev/null +++ b/e2e/cypress/e2e/a11y/bal-dropdown.a11y.cy.ts @@ -0,0 +1,20 @@ +import { selectors } from 'support/utils' + +describe('bal-dropdown', () => { + beforeEach(() => { + cy.pageA11y('/components/bal-dropdown/test/bal-dropdown.a11y.html') + cy.platform('desktop') + cy.waitForDesignSystem() + }) + + it('closed state', () => { + cy.get('main').testA11y() + }) + + it('open state', () => { + cy.getByLabelText('Year').click() + cy.get('main').testA11y() + cy.getByRole('option', { name: 'v1990' }).click() + cy.get('main').testA11y() + }) +}) diff --git a/e2e/cypress/e2e/a11y/bal-option-list.a11y.cy.ts b/e2e/cypress/e2e/a11y/bal-option-list.a11y.cy.ts new file mode 100644 index 0000000000..0af30b90ef --- /dev/null +++ b/e2e/cypress/e2e/a11y/bal-option-list.a11y.cy.ts @@ -0,0 +1,9 @@ +describe('bal-option-list', () => { + context('a11y', () => { + beforeEach(() => cy.platform('desktop').pageA11y('/components/bal-option-list/test/bal-option-list.a11y.html')) + + describe('have the AA standard', () => { + it('basic', () => cy.getByTestId('basic').testA11y()) + }) + }) +}) diff --git a/e2e/cypress/e2e/a11y/bal-option.a11y.cy.ts b/e2e/cypress/e2e/a11y/bal-option.a11y.cy.ts new file mode 100644 index 0000000000..7fbfd18a0b --- /dev/null +++ b/e2e/cypress/e2e/a11y/bal-option.a11y.cy.ts @@ -0,0 +1,13 @@ +describe('bal-option', () => { + context('a11y', () => { + beforeEach(() => cy.platform('desktop').pageA11y('/components/bal-option/test/bal-option.a11y.html')) + + describe('have the AA standard', () => { + it('basic', () => cy.getByTestId('basic').testA11y()) + it('selected', () => cy.getByTestId('selected').testA11y()) + it('focused', () => cy.getByTestId('focused').testA11y()) + it('invalid', () => cy.getByTestId('invalid').testA11y()) + it('disabled', () => cy.getByTestId('disabled').testA11y()) + }) + }) +}) diff --git a/e2e/cypress/e2e/base/bal-dropdown.cy.ts b/e2e/cypress/e2e/base/bal-dropdown.cy.ts new file mode 100644 index 0000000000..63de125c06 --- /dev/null +++ b/e2e/cypress/e2e/base/bal-dropdown.cy.ts @@ -0,0 +1,44 @@ +describe('bal-dropdown', () => { + beforeEach(() => { + cy.visit('/components/bal-dropdown/test/bal-dropdown.cy.html') + cy.waitForDesignSystem() + }) + + it('should select label or value', () => { + cy.getByTestId('basic').should('have.value', '1995') + cy.getByTestId('basic').should('not.have.value', '1994') + + cy.getByTestId('basic').click().select('1996').should('have.value', '1996') + cy.getByTestId('basic').click().select('v1997').should('have.value', '1997') + }) + + it('should be disabled', () => { + cy.getByTestId('basic').should('not.be.disabled') + cy.getByTestId('disabled').should('be.disabled') + }) + + it('should assert option labels', () => { + cy.getByTestId('basic').balSelectFindOptions().should('have.length', 6) + cy.getByTestId('basic').balSelectShouldHaveOptions(['1995', '1996', '1997', '1998', '1999', '2000']) + cy.getByTestId('basic').balSelectShouldHaveOptions(['v1995', 'v1996', 'v1997', 'v1998', 'v1999', 'v2000'], 'value') + }) + + describe('multiple', () => { + it('should select and deselect values', () => { + cy.getByTestId('multiple') + .click() + .select(['Black Widow', 'Black Panter']) + .should('have.value', ['Black Widow', 'Black Panter']) + + cy.getByTestId('multiple').balSelectFindOptions().first().click() + cy.getByTestId('multiple').balSelectFindOptions().eq(1).click() + cy.getByTestId('multiple').balSelectFindOptions().eq(2).click() + cy.getByTestId('multiple').should('have.value', ['Iron Man']) + cy.getByTestId('multiple').click() + + cy.getByTestId('multiple').balSelectFindChips().first().contains('Iron Man') + cy.getByTestId('multiple').balSelectFindChips().first().click() + cy.getByTestId('multiple').should('have.value', '') + }) + }) +}) diff --git a/e2e/cypress/e2e/visual/bal-dropdown.visual.cy.ts b/e2e/cypress/e2e/visual/bal-dropdown.visual.cy.ts new file mode 100644 index 0000000000..cbe171be8e --- /dev/null +++ b/e2e/cypress/e2e/visual/bal-dropdown.visual.cy.ts @@ -0,0 +1,77 @@ +import { testOnPlatforms } from 'support/utils' + +describe('bal-dropdown', () => { + beforeEach(() => { + cy.visit('/components/bal-dropdown/test/bal-dropdown.visual.html') + cy.waitForDesignSystem() + }) + + testOnPlatforms(['desktop', 'mobile'], platform => { + it('basic', () => { + cy.getByTestId('basic').testVisual(`dropdown-${platform}-basic-empty-closed`) + cy.getByPlaceholder('visual-basic').click() + cy.getByTestId('basic').testVisual(`dropdown-${platform}-basic-empty-open`) + cy.getByTestId('basic').within(() => { + cy.getByRole('option', { name: '1992' }).click() + }) + cy.getByTestId('basic').testVisual(`dropdown-${platform}-basic-empty-selected`) + }) + + it('long-content', () => { + cy.getByTestId('long-content').testVisual(`dropdown-${platform}-long-content-empty-closed`) + cy.getByPlaceholder('visual-long-content').click() + cy.getByTestId('long-content').testVisual(`dropdown-${platform}-long-content-empty-open`) + }) + + it('multiple', () => { + cy.getByTestId('multiple').testVisual(`dropdown-${platform}-multiple-empty-closed`) + cy.getByPlaceholder('visual-multiple').click() + cy.getByTestId('multiple').testVisual(`dropdown-${platform}-multiple-empty-open`) + cy.getByTestId('multiple').within(() => { + cy.getByRole('option', { name: '1991' }).click() + cy.getByRole('option', { name: '1992' }).click() + }) + cy.getByTestId('multiple').testVisual(`dropdown-${platform}-multiple-empty-selected`) + }) + + it('multiple-chips', () => { + cy.getByTestId('multiple-chips').testVisual(`dropdown-${platform}-multiple-chips-empty-closed`) + cy.getByPlaceholder('visual-multiple-chips').click({ force: true }) + cy.getByTestId('multiple-chips').testVisual(`dropdown-${platform}-multiple-chips-empty-open`) + }) + + it('form-field', () => { + cy.getByTestId('form-field').testVisual(`dropdown-${platform}-form-field-empty-closed`) + cy.getByPlaceholder('visual-form-field').click() + cy.getByTestId('form-field').testVisual(`dropdown-${platform}-form-field-empty-open`) + }) + }) + + context('states', () => { + beforeEach(() => { + cy.platform('desktop') + }) + + it('clearable', () => { + cy.getByTestId('clearable').testVisual(`dropdown-clearable-empty-closed`) + cy.getByPlaceholder('visual-clearable').click() + cy.getByTestId('multiple').testVisual(`dropdown-clearable-empty-open`) + cy.getByTestId('clearable').within(() => { + cy.getByRole('option', { name: '1988' }).click() + }) + cy.getByTestId('clearable').testVisual(`dropdown-clearable-empty-selected`) + }) + + it('loading', () => { + cy.getByTestId('loading').testVisual(`dropdown-loading-empty-closed`) + }) + + it('invalid', () => { + cy.getByTestId('invalid').testVisual(`dropdown-invalid-empty-closed`) + }) + + it('disabled', () => { + cy.getByTestId('disabled').testVisual(`dropdown-disabled-empty-closed`) + }) + }) +}) diff --git a/e2e/cypress/e2e/visual/bal-option-list.visual.cy.ts b/e2e/cypress/e2e/visual/bal-option-list.visual.cy.ts new file mode 100644 index 0000000000..fc14eb7937 --- /dev/null +++ b/e2e/cypress/e2e/visual/bal-option-list.visual.cy.ts @@ -0,0 +1,9 @@ +describe('bal-option-list', () => { + describe('basic', () => { + beforeEach(() => cy.visit('/components/bal-option-list/test/bal-option-list.visual.html').waitForDesignSystem()) + + it('basic component', () => { + cy.getByTestId('basic').testVisual('option-basic') + }) + }) +}) diff --git a/e2e/cypress/e2e/visual/bal-option.visual.cy.ts b/e2e/cypress/e2e/visual/bal-option.visual.cy.ts new file mode 100644 index 0000000000..c606b784be --- /dev/null +++ b/e2e/cypress/e2e/visual/bal-option.visual.cy.ts @@ -0,0 +1,17 @@ +describe('bal-option', () => { + describe('basic', () => { + beforeEach(() => cy.visit('/components/bal-option/test/bal-option.visual.html').waitForDesignSystem()) + + it('basic component', () => { + cy.getByTestId('basic').testVisual('option-basic') + }) + + it('listbox', () => { + cy.getByTestId('listbox').testVisual('option-listbox') + }) + + it('listbox-with-checkboxes', () => { + cy.getByTestId('listbox-checkbox').testVisual('option-listbox-checkbox') + }) + }) +}) diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-clearable-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-clearable-empty-closed.png new file mode 100644 index 0000000000..b0ccd4b280 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-clearable-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-clearable-empty-open.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-clearable-empty-open.png new file mode 100644 index 0000000000..7c6f2b5117 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-clearable-empty-open.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-clearable-empty-selected.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-clearable-empty-selected.png new file mode 100644 index 0000000000..673a212874 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-clearable-empty-selected.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-basic-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-basic-empty-closed.png new file mode 100644 index 0000000000..d0028e258c Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-basic-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-basic-empty-open.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-basic-empty-open.png new file mode 100644 index 0000000000..a892729d8e Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-basic-empty-open.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-basic-empty-selected.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-basic-empty-selected.png new file mode 100644 index 0000000000..1cc4fd931e Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-basic-empty-selected.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-form-field-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-form-field-empty-closed.png new file mode 100644 index 0000000000..05397ea2a4 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-form-field-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-form-field-empty-open.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-form-field-empty-open.png new file mode 100644 index 0000000000..be0e2c5492 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-form-field-empty-open.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-long-content-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-long-content-empty-closed.png new file mode 100644 index 0000000000..104976802d Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-long-content-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-long-content-empty-open.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-long-content-empty-open.png new file mode 100644 index 0000000000..ba0954c843 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-long-content-empty-open.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-chips-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-chips-empty-closed.png new file mode 100644 index 0000000000..a6f5df16d0 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-chips-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-chips-empty-open.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-chips-empty-open.png new file mode 100644 index 0000000000..01094ea2a2 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-chips-empty-open.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-empty-closed.png new file mode 100644 index 0000000000..7c6f2b5117 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-empty-open.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-empty-open.png new file mode 100644 index 0000000000..8982f1364a Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-empty-open.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-empty-selected.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-empty-selected.png new file mode 100644 index 0000000000..d9adfd9b31 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-desktop-multiple-empty-selected.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-disabled-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-disabled-empty-closed.png new file mode 100644 index 0000000000..590d7c5ff2 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-disabled-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-invalid-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-invalid-empty-closed.png new file mode 100644 index 0000000000..1a2a912a28 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-invalid-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-loading-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-loading-empty-closed.png new file mode 100644 index 0000000000..7955c31da6 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-loading-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-basic-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-basic-empty-closed.png new file mode 100644 index 0000000000..ab3b0ab90b Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-basic-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-basic-empty-open.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-basic-empty-open.png new file mode 100644 index 0000000000..1db9647f17 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-basic-empty-open.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-basic-empty-selected.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-basic-empty-selected.png new file mode 100644 index 0000000000..056be2cbd1 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-basic-empty-selected.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-form-field-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-form-field-empty-closed.png new file mode 100644 index 0000000000..33bda00a37 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-form-field-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-form-field-empty-open.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-form-field-empty-open.png new file mode 100644 index 0000000000..540c9d5ce8 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-form-field-empty-open.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-long-content-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-long-content-empty-closed.png new file mode 100644 index 0000000000..6dc25a8866 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-long-content-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-long-content-empty-open.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-long-content-empty-open.png new file mode 100644 index 0000000000..b5fda3cfec Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-long-content-empty-open.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-chips-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-chips-empty-closed.png new file mode 100644 index 0000000000..ec93aece61 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-chips-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-chips-empty-open.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-chips-empty-open.png new file mode 100644 index 0000000000..3e533b3d25 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-chips-empty-open.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-empty-closed.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-empty-closed.png new file mode 100644 index 0000000000..b163c668e9 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-empty-closed.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-empty-open.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-empty-open.png new file mode 100644 index 0000000000..a92459dd6d Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-empty-open.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-empty-selected.png b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-empty-selected.png new file mode 100644 index 0000000000..0226b50755 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-dropdown.visual.cy.ts/dropdown-mobile-multiple-empty-selected.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-option-list.visual.cy.ts/option-basic.png b/e2e/cypress/snapshots/base/visual/bal-option-list.visual.cy.ts/option-basic.png new file mode 100644 index 0000000000..b89e6ccf1a Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-option-list.visual.cy.ts/option-basic.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-option.visual.cy.ts/option-basic.png b/e2e/cypress/snapshots/base/visual/bal-option.visual.cy.ts/option-basic.png new file mode 100644 index 0000000000..5029bdc0a7 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-option.visual.cy.ts/option-basic.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-option.visual.cy.ts/option-listbox-checkbox.png b/e2e/cypress/snapshots/base/visual/bal-option.visual.cy.ts/option-listbox-checkbox.png new file mode 100644 index 0000000000..d4bfc56f94 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-option.visual.cy.ts/option-listbox-checkbox.png differ diff --git a/e2e/cypress/snapshots/base/visual/bal-option.visual.cy.ts/option-listbox.png b/e2e/cypress/snapshots/base/visual/bal-option.visual.cy.ts/option-listbox.png new file mode 100644 index 0000000000..0f661d9794 Binary files /dev/null and b/e2e/cypress/snapshots/base/visual/bal-option.visual.cy.ts/option-listbox.png differ diff --git a/e2e/cypress/snapshots/report.html b/e2e/cypress/snapshots/report.html deleted file mode 100644 index 0f64eae156..0000000000 --- a/e2e/cypress/snapshots/report.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - Comparison Report - REG - - -
- - - - - diff --git a/e2e/cypress/support/lib/visuals/command.ts b/e2e/cypress/support/lib/visuals/command.ts index 0f6e421d0a..ac63a28462 100644 --- a/e2e/cypress/support/lib/visuals/command.ts +++ b/e2e/cypress/support/lib/visuals/command.ts @@ -35,6 +35,8 @@ export type CypressConfigEnv = { /** Add custom cypress command to compare image snapshots of an element or the window. */ function addCompareSnapshotCommand(screenshotOptions?: ScreenshotOptions): void { + console.log('=> visualRegressionType is set to ', Cypress.env('visualRegressionType')) + Cypress.Commands.add( 'testVisual', { prevSubject: 'optional' }, @@ -96,6 +98,11 @@ function prepareOptions( 'Environment variables under "visualRegression" apper to be missing. Please consult the plugin documentation for the proper setup.', ) } + if (Cypress.env('visualRegressionType') === undefined) { + throw new Error( + 'Environment variables under "visualRegressionType" apper to be missing. Please consult the plugin documentation for the proper setup.', + ) + } const options: VisualRegressionOptions = { type: (Cypress.env('visualRegressionType') as TypeOption) || 'regression', screenshotName: name, diff --git a/e2e/project.json b/e2e/project.json index 5c471efc92..ada9421d07 100644 --- a/e2e/project.json +++ b/e2e/project.json @@ -6,8 +6,7 @@ "executor": "nx:run-commands", "dependsOn": [{ "projects": ["core"], "target": "build" }], "options": { - "command": "concurrently --prefix=\">\" --hide=\"1\" \"npx nx run core:serve\" \"npx nx run e2e:serve\"", - "parallel": true + "command": "concurrently --prefix=\">\" --hide=\"1\" \"npx nx run core:serve\" \"npx nx run e2e:serve\"" } }, "serve": { diff --git a/packages/angular-legacy/src/app-initialize.ts b/packages/angular-legacy/src/app-initialize.ts index 7e2eea8040..59f16ead0a 100644 --- a/packages/angular-legacy/src/app-initialize.ts +++ b/packages/angular-legacy/src/app-initialize.ts @@ -11,7 +11,14 @@ export const appInitialize = (config: BaloiseDesignSystemAngularConfig, doc: Doc const win: Window | undefined = doc.defaultView as any if (win && typeof (window as any) !== 'undefined') { - initializeBaloiseDesignSystem(config.defaults, undefined, win) + initializeBaloiseDesignSystem( + { + ...config.defaults, + httpFormSubmit: false, + }, + undefined, + win, + ) const aelFn = '__zone_symbol__addEventListener' in (doc.body as any) ? '__zone_symbol__addEventListener' : 'addEventListener' diff --git a/packages/angular-module/src/app-initialize.ts b/packages/angular-module/src/app-initialize.ts index f4d480a7c3..3186705684 100644 --- a/packages/angular-module/src/app-initialize.ts +++ b/packages/angular-module/src/app-initialize.ts @@ -31,7 +31,14 @@ export const appInitialize = (config: BaloiseDesignSystemAngularConfig, doc: Doc }, } - initializeBaloiseDesignSystem(config.defaults, platformConfig, win) + initializeBaloiseDesignSystem( + { + ...config.defaults, + httpFormSubmit: false, + }, + platformConfig, + win, + ) } } } diff --git a/packages/angular/src/app-initialize.ts b/packages/angular/src/app-initialize.ts index fc55184250..5da4ee2406 100644 --- a/packages/angular/src/app-initialize.ts +++ b/packages/angular/src/app-initialize.ts @@ -30,7 +30,14 @@ export const appInitialize = (config: BaloiseDesignSystemAngularConfig, doc: Doc }, } - initializeBaloiseDesignSystem(config.defaults, platformConfig, win) + initializeBaloiseDesignSystem( + { + ...config.defaults, + httpFormSubmit: false, + }, + platformConfig, + win, + ) } } } diff --git a/packages/angular/src/bundles.ts b/packages/angular/src/bundles.ts index 4c351304a6..2772da378a 100644 --- a/packages/angular/src/bundles.ts +++ b/packages/angular/src/bundles.ts @@ -3,6 +3,7 @@ import { BalCheckboxGroup, BalDate, BalDatepicker, + BalDropdown, BalInput, BalInputDate, BalInputSlider, @@ -87,6 +88,8 @@ import { BalTag, BalTagGroup, BalText, + BalOption, + BalOptionList, } from './generated/proxies' export const BalAccordionBundle = [BalAccordion, BalAccordionDetails, BalAccordionSummary, BalAccordionTrigger] as const @@ -155,6 +158,8 @@ export const BalFieldBundle = [ BalFieldHint, ] as const +export const BalDropdownBundle = [BalDropdown, BalOptionList, BalOption] as const + /* Component Sections */ export const BalFormBundle = [ @@ -171,6 +176,7 @@ export const BalFormBundle = [ ...BalCheckboxBundle, BalDate, BalDatepicker, + ...BalDropdownBundle, BalInputDate, BalInputStepper, BalInputSlider, @@ -209,6 +215,9 @@ export const BalComponentBundle = [ BalDataLabel, BalDataValue, BalDivider, + BalDropdown, + BalOption, + BalOptionList, BalField, BalFieldControl, BalFieldHint, diff --git a/packages/angular/src/components/bal-dropdown.ts b/packages/angular/src/components/bal-dropdown.ts new file mode 100644 index 0000000000..1df694f0fe --- /dev/null +++ b/packages/angular/src/components/bal-dropdown.ts @@ -0,0 +1,75 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Injector, + NgZone, + forwardRef, +} from '@angular/core' +import { NG_VALUE_ACCESSOR } from '@angular/forms' + +import type { Components } from '@baloise/ds-core' +import { defineCustomElement as defineOptionDropdown } from '@baloise/ds-core/components/bal-dropdown' +import { defineCustomElement as defineOptionList } from '@baloise/ds-core/components/bal-option-list' +import { defineCustomElement as defineOption } from '@baloise/ds-core/components/bal-option' + +import { ProxyCmp, proxyOutputs } from '../generated/angular-component-lib/utils' +import { ValueAccessor } from '../generated/value-accessor' +import { BalDropdownInputs, BalDropdownMethods, BalDropdownOutputs } from '../generated/meta' + +const accessorProvider = { + provide: NG_VALUE_ACCESSOR, + useExisting: /*@__PURE__*/ forwardRef(() => BalDropdown), + multi: true, +} + +@ProxyCmp({ + defineCustomElementFn: () => { + defineOption() + defineOptionList() + defineOptionDropdown() + }, + inputs: BalDropdownInputs, + methods: BalDropdownMethods, +}) +@Component({ + selector: 'bal-dropdown', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + providers: [accessorProvider], + standalone: true, + inputs: BalDropdownInputs, + outputs: BalDropdownOutputs, +}) +export class BalDropdown extends ValueAccessor { + protected el: HTMLElement + + constructor( + c: ChangeDetectorRef, + r: ElementRef, + protected z: NgZone, + injector: Injector, + ) { + super(injector, r) + c.detach() + this.el = r.nativeElement + proxyOutputs(this, this.el, BalDropdownOutputs) + } + + @HostListener('balChange', ['$event']) + handleBalChange(event: CustomEvent): void { + this.handleValueChange(event) + } +} + +export declare interface BalDropdown extends Components.BalDropdown { + /** Emitted when a option got selected. */ + balChange: EventEmitter> + /** Emitted when the input loses focus. */ + balBlur: EventEmitter> + /** Emitted when the input has focus. */ + balFocus: EventEmitter> +} diff --git a/packages/angular/src/components/index.ts b/packages/angular/src/components/index.ts index 8cf50209c2..fe1a2a9b97 100644 --- a/packages/angular/src/components/index.ts +++ b/packages/angular/src/components/index.ts @@ -10,5 +10,6 @@ export * from './bal-input' export * from './bal-number-input' export * from './bal-radio-group' export * from './bal-select' +export * from './bal-dropdown' export * from './bal-textarea' export * from './bal-time-input' diff --git a/packages/core/config/stencil.bindings.angular.ts b/packages/core/config/stencil.bindings.angular.ts index a8ede96e04..87979219f6 100644 --- a/packages/core/config/stencil.bindings.angular.ts +++ b/packages/core/config/stencil.bindings.angular.ts @@ -7,6 +7,7 @@ export const angularValueAccessorBindings: ValueAccessorConfig[] = [ 'bal-radio-group', 'bal-checkbox-group', 'bal-select', + 'bal-dropdown', 'bal-datepicker', 'bal-date', 'bal-input-date', @@ -49,6 +50,7 @@ export const AngularGenerator = () => 'bal-checkbox', 'bal-date', 'bal-datepicker', + 'bal-dropdown', 'bal-file-upload', 'bal-input-date', 'bal-input-slider', diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 49461c5925..76a731a177 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -10,6 +10,7 @@ import { AccordionState, BalAriaForm as BalAriaForm1, BalConfigState as BalConfi import { BalCarouselItemData } from "./components/bal-carousel/bal-carousel.type"; import { BalCheckboxOption } from "./components/bal-checkbox/bal-checkbox.type"; import { BalAriaForm } from "./utils/form"; +import { BalOption } from "./utils/dropdown"; import { OverlayEventDetail } from "./components/bal-modal/bal-modal.type"; import { PopoverPresentOptions } from "./components/bal-popover/bal-popover"; import { BalRadioOption } from "./components/bal-radio/bal-radio.type"; @@ -20,6 +21,7 @@ export { AccordionState, BalAriaForm as BalAriaForm1, BalConfigState as BalConfi export { BalCarouselItemData } from "./components/bal-carousel/bal-carousel.type"; export { BalCheckboxOption } from "./components/bal-checkbox/bal-checkbox.type"; export { BalAriaForm } from "./utils/form"; +export { BalOption } from "./utils/dropdown"; export { OverlayEventDetail } from "./components/bal-modal/bal-modal.type"; export { PopoverPresentOptions } from "./components/bal-popover/bal-popover"; export { BalRadioOption } from "./components/bal-radio/bal-radio.type"; @@ -965,6 +967,99 @@ export namespace Components { "region"?: string; "stickyFooter": boolean; } + interface BalDropdown { + /** + * Indicates whether the value of the control can be automatically completed by the browser. + */ + "autocomplete": BalProps.BalInputAutocomplete; + /** + * If `true`, the selected options are shown as chips + */ + "chips": boolean; + /** + * Sets the value to `[]`, the input value to ´''´ and the focus index to ´0´. + */ + "clear": () => Promise; + /** + * If `true`, a cross at the end is visible to clear the selection + */ + "clearable": boolean; + /** + * Closes the popup with option list + */ + "close": () => Promise; + "configChanged": (state: BalConfigState) => Promise; + /** + * Defines the max height of the list element + */ + "contentHeight": number; + /** + * If `true`, the user cannot interact with the option. + */ + "disabled": boolean; + /** + * Defines the filter logic of the list + */ + "filter": BalProps.BalOptionListFilter; + /** + * Returns the value of the component + */ + "getValue": () => Promise; + /** + * If `true` there will be on trigger icon visible + */ + "icon": string; + /** + * If `true`, the component will be shown as invalid + */ + "invalid": boolean; + "inverted": boolean; + /** + * Defines if the select is in a loading state. + */ + "loading": boolean; + /** + * If `true`, the user can select multiple options. + */ + "multiple": boolean; + /** + * The name of the control, which is submitted with the form data. + */ + "name": string; + /** + * Opens the popup with option list + */ + "open": () => Promise; + /** + * Steps can be passed as a property or through HTML markup. + */ + "options": BalOption[]; + /** + * Defines the placeholder of the component. Only shown when the value is empty + */ + "placeholder": string; + /** + * If `true` the element can not mutated, meaning the user can not edit the control. + */ + "readonly": boolean; + /** + * If `true`, the user must fill in a value before submitting a form. + */ + "required": boolean; + /** + * Select option by passed value + */ + "select": (newValue: string | string[]) => Promise; + "setAriaForm": (ariaForm: BalAriaForm) => Promise; + /** + * Sets the focus on the input element + */ + "setFocus": () => Promise; + /** + * The value of the selected options. + */ + "value"?: string | string[]; + } interface BalField { /** * If `true`, the element is not mutable, focusable, or even submitted with the form. The user can neither edit nor focus on the control, nor its form control descendants. @@ -2080,6 +2175,145 @@ export namespace Components { */ "value"?: number; } + interface BalOption { + /** + * If `true`, the user cannot interact with the option. + */ + "disabled": boolean; + /** + * If `true`, the option is focused. + */ + "focused": boolean; + /** + * If `true`, the option is hidden. + */ + "hidden": boolean; + /** + * If `true`, the option is shown in red. + */ + "invalid": boolean; + /** + * Label will be shown in the input element when it got selected + */ + "label": string; + /** + * If `true`, the option can present in more than one line. + */ + "multiline": boolean; + /** + * Selects or deselects the option and informs other components + */ + "select": (selected?: boolean) => Promise; + /** + * If `true`, the option is selected. + */ + "selected": boolean; + /** + * The value of the select option. This value will be returned by the parent `` element. + */ + "value": string; + } + interface BalOptionList { + /** + * Defines the max height of the list element + */ + "contentHeight"?: number; + /** + * If `true`, the user cannot interact with the option. + */ + "disabled": boolean; + /** + * Defines the filter logic of the list + */ + "filter": BalProps.BalOptionListFilter; + /** + * Filter the options by the given filter property and hides options + * @returns focusIndex + */ + "filterByContent": (search: string) => Promise; + /** + * Focus the option with the label that starts with the search property + * @returns focusIndex + */ + "focusByLabel": (search: string, config: Partial<{ select: boolean; }>) => Promise; + /** + * Focus the first visible option in the list + * @returns focusIndex + */ + "focusFirst": () => Promise; + /** + * Defines the focused option with his index value + */ + "focusIndex": number; + /** + * Focus the last visible option in the list + * @returns focusIndex + */ + "focusLast": () => Promise; + /** + * Focus the next visible option in the list + * @returns focusIndex + */ + "focusNext": () => Promise; + /** + * Focus the previous visible option in the list + * @returns focusIndex + */ + "focusPrevious": () => Promise; + /** + * Returns a list of options + */ + "getLabels": () => Promise; + /** + * Returns a list of accessible options + */ + "getOptions": () => Promise; + /** + * Returns a list of option labels + */ + "getSelectedOptions": (values?: string[]) => Promise; + /** + * Returns a list of option values + */ + "getSelectedValues": () => Promise; + /** + * Returns a list of options + */ + "getValues": () => Promise; + /** + * Id of the label element to describe this option list + */ + "labelledby"?: string; + /** + * If `true` the list supports multiple selections + */ + "multiple": boolean; + /** + * If `true`, the user must fill in a value before submitting a form. + */ + "required": boolean; + /** + * Resets the focus index to pristine and scrolls to the top of the list + */ + "resetFocus": () => Promise; + /** + * Shows or hides all options + */ + "resetHidden": (hidden?: boolean) => Promise; + /** + * Selects or deselects all options + */ + "resetSelected": (selected?: boolean) => Promise; + /** + * Selects the option with the current focus + */ + "selectByFocus": () => Promise; + "setAriaForm": (ariaForm: BalAriaForm) => Promise; + /** + * Updates options + */ + "updateSelected": (values?: string[]) => Promise; + } interface BalPagination { /** * Align the buttons to start, center or end @@ -2493,7 +2727,7 @@ export namespace Components { */ "freeSolo": boolean; /** - * Sets the focus on the input element + * Returns the value of the component */ "getValue": () => Promise; /** @@ -2650,7 +2884,7 @@ export namespace Components { /** * Defines the color of the spinner. */ - "color": 'blue' | 'white'; + "color": BalProps.BalSpinnerColor; /** * If `true` the component will not add the spinner animation svg */ @@ -2663,6 +2897,10 @@ export namespace Components { * If `true` the component is smaller */ "small": boolean; + /** + * Defines the look of the spinner + */ + "variation": BalProps.BalSpinnerVariation; } interface BalStack { /** @@ -3284,6 +3522,10 @@ export interface BalDatepickerCustomEvent extends CustomEvent { detail: T; target: HTMLBalDatepickerElement; } +export interface BalDropdownCustomEvent extends CustomEvent { + detail: T; + target: HTMLBalDropdownElement; +} export interface BalFieldCustomEvent extends CustomEvent { detail: T; target: HTMLBalFieldElement; @@ -3332,6 +3574,10 @@ export interface BalNumberInputCustomEvent extends CustomEvent { detail: T; target: HTMLBalNumberInputElement; } +export interface BalOptionCustomEvent extends CustomEvent { + detail: T; + target: HTMLBalOptionElement; +} export interface BalPaginationCustomEvent extends CustomEvent { detail: T; target: HTMLBalPaginationElement; @@ -3747,6 +3993,25 @@ declare global { prototype: HTMLBalDocAppElement; new (): HTMLBalDocAppElement; }; + interface HTMLBalDropdownElementEventMap { + "balChange": BalEvents.BalDropdownChangeDetail; + "balFocus": BalEvents.BalDropdownFocusDetail; + "balBlur": BalEvents.BalDropdownBlurDetail; + } + interface HTMLBalDropdownElement extends Components.BalDropdown, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLBalDropdownElement, ev: BalDropdownCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLBalDropdownElement, ev: BalDropdownCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLBalDropdownElement: { + prototype: HTMLBalDropdownElement; + new (): HTMLBalDropdownElement; + }; interface HTMLBalFieldElementEventMap { "balFormControlDidLoad": BalEvents.BalFieldAriaLabelledByDetail; } @@ -4208,6 +4473,30 @@ declare global { prototype: HTMLBalNumberInputElement; new (): HTMLBalNumberInputElement; }; + interface HTMLBalOptionElementEventMap { + "balOptionChange": BalEvents.BalOptionChangeDetail; + "balOptionFocus": BalEvents.BalOptionFocusDetail; + } + interface HTMLBalOptionElement extends Components.BalOption, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLBalOptionElement, ev: BalOptionCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLBalOptionElement, ev: BalOptionCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLBalOptionElement: { + prototype: HTMLBalOptionElement; + new (): HTMLBalOptionElement; + }; + interface HTMLBalOptionListElement extends Components.BalOptionList, HTMLStencilElement { + } + var HTMLBalOptionListElement: { + prototype: HTMLBalOptionListElement; + new (): HTMLBalOptionListElement; + }; interface HTMLBalPaginationElementEventMap { "balChange": BalEvents.BalPaginationChangeDetail; } @@ -4655,6 +4944,7 @@ declare global { "bal-datepicker": HTMLBalDatepickerElement; "bal-divider": HTMLBalDividerElement; "bal-doc-app": HTMLBalDocAppElement; + "bal-dropdown": HTMLBalDropdownElement; "bal-field": HTMLBalFieldElement; "bal-field-control": HTMLBalFieldControlElement; "bal-field-hint": HTMLBalFieldHintElement; @@ -4704,6 +4994,8 @@ declare global { "bal-notices": HTMLBalNoticesElement; "bal-notification": HTMLBalNotificationElement; "bal-number-input": HTMLBalNumberInputElement; + "bal-option": HTMLBalOptionElement; + "bal-option-list": HTMLBalOptionListElement; "bal-pagination": HTMLBalPaginationElement; "bal-popover": HTMLBalPopoverElement; "bal-popover-content": HTMLBalPopoverContentElement; @@ -5727,6 +6019,85 @@ declare namespace LocalJSX { "region"?: string; "stickyFooter"?: boolean; } + interface BalDropdown { + /** + * Indicates whether the value of the control can be automatically completed by the browser. + */ + "autocomplete"?: BalProps.BalInputAutocomplete; + /** + * If `true`, the selected options are shown as chips + */ + "chips"?: boolean; + /** + * If `true`, a cross at the end is visible to clear the selection + */ + "clearable"?: boolean; + /** + * Defines the max height of the list element + */ + "contentHeight"?: number; + /** + * If `true`, the user cannot interact with the option. + */ + "disabled"?: boolean; + /** + * Defines the filter logic of the list + */ + "filter"?: BalProps.BalOptionListFilter; + /** + * If `true` there will be on trigger icon visible + */ + "icon"?: string; + /** + * If `true`, the component will be shown as invalid + */ + "invalid"?: boolean; + "inverted"?: boolean; + /** + * Defines if the select is in a loading state. + */ + "loading"?: boolean; + /** + * If `true`, the user can select multiple options. + */ + "multiple"?: boolean; + /** + * The name of the control, which is submitted with the form data. + */ + "name"?: string; + /** + * Emitted when the input loses focus. + */ + "onBalBlur"?: (event: BalDropdownCustomEvent) => void; + /** + * Emitted when a option got selected. + */ + "onBalChange"?: (event: BalDropdownCustomEvent) => void; + /** + * Emitted when the input has focus. + */ + "onBalFocus"?: (event: BalDropdownCustomEvent) => void; + /** + * Steps can be passed as a property or through HTML markup. + */ + "options"?: BalOption[]; + /** + * Defines the placeholder of the component. Only shown when the value is empty + */ + "placeholder"?: string; + /** + * If `true` the element can not mutated, meaning the user can not edit the control. + */ + "readonly"?: boolean; + /** + * If `true`, the user must fill in a value before submitting a form. + */ + "required"?: boolean; + /** + * The value of the selected options. + */ + "value"?: string | string[]; + } interface BalField { /** * If `true`, the element is not mutable, focusable, or even submitted with the form. The user can neither edit nor focus on the control, nor its form control descendants. @@ -6902,6 +7273,75 @@ declare namespace LocalJSX { */ "value"?: number; } + interface BalOption { + /** + * If `true`, the user cannot interact with the option. + */ + "disabled"?: boolean; + /** + * If `true`, the option is focused. + */ + "focused"?: boolean; + /** + * If `true`, the option is hidden. + */ + "hidden"?: boolean; + /** + * If `true`, the option is shown in red. + */ + "invalid"?: boolean; + /** + * Label will be shown in the input element when it got selected + */ + "label"?: string; + /** + * If `true`, the option can present in more than one line. + */ + "multiline"?: boolean; + /** + * Emitted when the option gets selected or unselected + */ + "onBalOptionChange"?: (event: BalOptionCustomEvent) => void; + "onBalOptionFocus"?: (event: BalOptionCustomEvent) => void; + /** + * If `true`, the option is selected. + */ + "selected"?: boolean; + /** + * The value of the select option. This value will be returned by the parent `` element. + */ + "value"?: string; + } + interface BalOptionList { + /** + * Defines the max height of the list element + */ + "contentHeight"?: number; + /** + * If `true`, the user cannot interact with the option. + */ + "disabled"?: boolean; + /** + * Defines the filter logic of the list + */ + "filter"?: BalProps.BalOptionListFilter; + /** + * Defines the focused option with his index value + */ + "focusIndex"?: number; + /** + * Id of the label element to describe this option list + */ + "labelledby"?: string; + /** + * If `true` the list supports multiple selections + */ + "multiple"?: boolean; + /** + * If `true`, the user must fill in a value before submitting a form. + */ + "required"?: boolean; + } interface BalPagination { /** * Align the buttons to start, center or end @@ -7479,7 +7919,7 @@ declare namespace LocalJSX { /** * Defines the color of the spinner. */ - "color"?: 'blue' | 'white'; + "color"?: BalProps.BalSpinnerColor; /** * If `true` the component will not add the spinner animation svg */ @@ -7492,6 +7932,10 @@ declare namespace LocalJSX { * If `true` the component is smaller */ "small"?: boolean; + /** + * Defines the look of the spinner + */ + "variation"?: BalProps.BalSpinnerVariation; } interface BalStack { /** @@ -8107,6 +8551,7 @@ declare namespace LocalJSX { "bal-datepicker": BalDatepicker; "bal-divider": BalDivider; "bal-doc-app": BalDocApp; + "bal-dropdown": BalDropdown; "bal-field": BalField; "bal-field-control": BalFieldControl; "bal-field-hint": BalFieldHint; @@ -8156,6 +8601,8 @@ declare namespace LocalJSX { "bal-notices": BalNotices; "bal-notification": BalNotification; "bal-number-input": BalNumberInput; + "bal-option": BalOption; + "bal-option-list": BalOptionList; "bal-pagination": BalPagination; "bal-popover": BalPopover; "bal-popover-content": BalPopoverContent; @@ -8226,6 +8673,7 @@ declare module "@stencil/core" { "bal-datepicker": LocalJSX.BalDatepicker & JSXBase.HTMLAttributes; "bal-divider": LocalJSX.BalDivider & JSXBase.HTMLAttributes; "bal-doc-app": LocalJSX.BalDocApp & JSXBase.HTMLAttributes; + "bal-dropdown": LocalJSX.BalDropdown & JSXBase.HTMLAttributes; "bal-field": LocalJSX.BalField & JSXBase.HTMLAttributes; "bal-field-control": LocalJSX.BalFieldControl & JSXBase.HTMLAttributes; "bal-field-hint": LocalJSX.BalFieldHint & JSXBase.HTMLAttributes; @@ -8275,6 +8723,8 @@ declare module "@stencil/core" { "bal-notices": LocalJSX.BalNotices & JSXBase.HTMLAttributes; "bal-notification": LocalJSX.BalNotification & JSXBase.HTMLAttributes; "bal-number-input": LocalJSX.BalNumberInput & JSXBase.HTMLAttributes; + "bal-option": LocalJSX.BalOption & JSXBase.HTMLAttributes; + "bal-option-list": LocalJSX.BalOptionList & JSXBase.HTMLAttributes; "bal-pagination": LocalJSX.BalPagination & JSXBase.HTMLAttributes; "bal-popover": LocalJSX.BalPopover & JSXBase.HTMLAttributes; "bal-popover-content": LocalJSX.BalPopoverContent & JSXBase.HTMLAttributes; diff --git a/packages/core/src/components/bal-checkbox/bal-checkbox.tsx b/packages/core/src/components/bal-checkbox/bal-checkbox.tsx index a0b8698ee9..fffaf108c9 100644 --- a/packages/core/src/components/bal-checkbox/bal-checkbox.tsx +++ b/packages/core/src/components/bal-checkbox/bal-checkbox.tsx @@ -20,6 +20,7 @@ import { BalCheckboxOption } from './bal-checkbox.type' import { Loggable, Logger, LogInstance } from '../../utils/log' import { FOCUS_KEYS } from '../../utils/focus-visible' import { BalAriaForm, BalAriaFormLinking, defaultBalAriaForm } from '../../utils/form' +import { ariaBooleanToString } from '../../utils/aria' @Component({ tag: 'bal-checkbox', @@ -424,8 +425,8 @@ export class Checkbox implements ComponentInterface, FormInput, Loggable, B return ( , Loggable, B aria-labelledby={labelId} aria-describedby={this.ariaForm.messageId} aria-invalid={this.invalid === true ? 'true' : 'false'} - aria-disabled={this.disabled ? 'true' : null} + aria-disabled={ariaBooleanToString(this.disabled)} aria-checked={`${this.checked}`} + aria-hidden={ariaBooleanToString(this.nonSubmit)} name={this.name} value={this.value} checked={this.checked} diff --git a/packages/core/src/components/bal-checkbox/bal-radio-checkbox.vars.sass b/packages/core/src/components/bal-checkbox/bal-radio-checkbox.vars.sass index 54ebe4288e..76e5e06324 100644 --- a/packages/core/src/components/bal-checkbox/bal-radio-checkbox.vars.sass +++ b/packages/core/src/components/bal-checkbox/bal-radio-checkbox.vars.sass @@ -59,9 +59,21 @@ * @prop --bal-radio-checkbox-button-border-color-checked: tbd * @prop --bal-radio-checkbox-button-border-color-disabled: tbd * @prop --bal-radio-checkbox-button-border-color-invalid: tbd + * @prop --bal-radio-checkbox-symbol-size: tbd + * @prop --bal-radio-checkbox-symbol-width: tbd + * @prop --bal-radio-checkbox-symbol-height: tbd + * @prop --bal-radio-checkbox-symbol-left: tbd + * @prop --bal-radio-checkbox-symbol-margin-top: tbd + * @prop --bal-radio-checkbox-label-min-height: tbd */ :root + --bal-radio-checkbox-symbol-size: 1.5rem + --bal-radio-checkbox-symbol-width: calc(0.5rem - 1px) + --bal-radio-checkbox-symbol-height: calc(0.875rem - 1px) + --bal-radio-checkbox-symbol-left: calc(0.5rem + 1px) + --bal-radio-checkbox-symbol-margin-top: 0.25rem + --bal-radio-checkbox-label-min-height: 1.5rem // // background colors --bal-radio-checkbox-select-button-background-hover: var(--bal-color-grey-2) diff --git a/packages/core/src/components/bal-checkbox/radio-checkbox.sass b/packages/core/src/components/bal-checkbox/radio-checkbox.sass index ec727b41af..eab59d5ef4 100644 --- a/packages/core/src/components/bal-checkbox/radio-checkbox.sass +++ b/packages/core/src/components/bal-checkbox/radio-checkbox.sass @@ -102,7 +102,7 @@ bal-radio .bal-radio-checkbox__label align-items: center justify-content: center text-align: left - min-height: 1.5rem + min-height: var(--bal-radio-checkbox-label-min-height) font-family: var(--bal-font-family-text) // // inner text span element @@ -134,12 +134,12 @@ bal-radio .bal-radio-checkbox__label position: absolute left: 0 top: 0.25rem - height: 1.5rem - width: 1.5rem + height: var(--bal-radio-checkbox-symbol-size) + width: var(--bal-radio-checkbox-symbol-size) background-color: transparent background-position: center background-repeat: no-repeat - background-size: 1.5rem 1.5rem + background-size: var(--bal-radio-checkbox-symbol-size) var(--bal-radio-checkbox-symbol-size) &::before content: '' border-style: var(--bal-form-field-control-border-style) @@ -171,10 +171,10 @@ bal-radio .bal-radio-checkbox__label border-bottom-width: 2px border-left: 0 border-radius: 1px - width: calc(0.5rem - 1px) - height: calc(0.875rem - 1px) - left: calc(0.5rem + 1px) - margin-top: 0.25rem + width: var(--bal-radio-checkbox-symbol-width) + height: var(--bal-radio-checkbox-symbol-height) + left: var(--bal-radio-checkbox-symbol-left) + margin-top: var(--bal-radio-checkbox-symbol-margin-top) transform: rotate(45deg) // // Symbol colors main, inverted and background diff --git a/packages/core/src/components/bal-close/bal-close.tsx b/packages/core/src/components/bal-close/bal-close.tsx index aab4766432..ea8fb9ebd8 100644 --- a/packages/core/src/components/bal-close/bal-close.tsx +++ b/packages/core/src/components/bal-close/bal-close.tsx @@ -53,6 +53,7 @@ export class Close implements ComponentInterface, BalConfigObserver { type="button" aria-label={label} title={label} + tabindex={-1} class={{ ...buttonEl.class(), ...buttonEl.modifier('inverted').class(this.inverted), diff --git a/packages/core/src/components/bal-date/bal-date-calendar/components/bal-date-calendar__list.tsx b/packages/core/src/components/bal-date/bal-date-calendar/components/bal-date-calendar__list.tsx index 86c20ddd07..df5c984a85 100644 --- a/packages/core/src/components/bal-date/bal-date-calendar/components/bal-date-calendar__list.tsx +++ b/packages/core/src/components/bal-date/bal-date-calendar/components/bal-date-calendar__list.tsx @@ -37,7 +37,7 @@ export const CalendarList: FunctionalComponent = ({ ref={el => (ref ? ref(el) : undefined)} > {list.map(item => ( -
  • +
  • + + Submit + Reset + diff --git a/packages/core/src/components/bal-spinner/bal-spinner.interfaces.ts b/packages/core/src/components/bal-spinner/bal-spinner.interfaces.ts new file mode 100644 index 0000000000..7c0e4a3a68 --- /dev/null +++ b/packages/core/src/components/bal-spinner/bal-spinner.interfaces.ts @@ -0,0 +1,11 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// + +namespace BalProps { + export type BalSpinnerColor = 'blue' | 'white' + export type BalSpinnerVariation = 'logo' | 'circle' +} + +namespace BalEvents {} diff --git a/packages/core/src/components/bal-spinner/bal-spinner.sass b/packages/core/src/components/bal-spinner/bal-spinner.sass index 53b8fc8845..350b788961 100644 --- a/packages/core/src/components/bal-spinner/bal-spinner.sass +++ b/packages/core/src/components/bal-spinner/bal-spinner.sass @@ -1,10 +1,44 @@ @import '@baloise/ds-styles/sass/mixins' +// Spinner +// -------------------------------------------------- + bal-spinner, .bal-spinner text-align: center display: flex justify-content: center align-content: center + width: 4rem svg transform: unset !important + +.bal-spinner--small + width: 2rem + +// Circle +// -------------------------------------------------- + +.bal-spinner--circle + margin: auto + border-width: 0.25rem + border-style: solid + border-color: var(--bal-color-grey) + border-radius: 50% + border-top-color: var(--bal-color-primary) + width: 1.5rem + height: 1.5rem + +.bal-spinner--circle.bal-spinner--small + width: 1.125rem + height: 1.125rem + border-width: 0.2rem + +.bal-spinner--circle.bal-spinner--animated + animation: spinner 1.6s linear infinite + +@keyframes spinner + 0% + transform: rotate(0deg) + 100% + transform: rotate(360deg) diff --git a/packages/core/src/components/bal-spinner/bal-spinner.tsx b/packages/core/src/components/bal-spinner/bal-spinner.tsx index e81a476b34..013b7ab2b2 100644 --- a/packages/core/src/components/bal-spinner/bal-spinner.tsx +++ b/packages/core/src/components/bal-spinner/bal-spinner.tsx @@ -1,8 +1,10 @@ -import { Component, h, Host, Prop, Element, Watch, ComponentInterface } from '@stencil/core' +import { Component, h, Host, Prop, Element, Watch, ComponentInterface, State } from '@stencil/core' import type { AnimationItem } from 'lottie-web/build/player/lottie_light_html' import { rIC } from '../../utils/helpers' import { Loggable, Logger, LogInstance } from '../../utils/log' import { raf } from '../../utils/helpers' +import { BEM } from '../../utils/bem' +import { BalConfigObserver, BalConfigState, ListenToConfig, defaultConfig } from '../../utils/config' type SpinnerAnimationFunction = (el: HTMLElement, color: string) => AnimationItem @@ -10,13 +12,15 @@ type SpinnerAnimationFunction = (el: HTMLElement, color: string) => AnimationIte tag: 'bal-spinner', styleUrl: 'bal-spinner.sass', }) -export class Spinner implements ComponentInterface, Loggable { +export class Spinner implements ComponentInterface, Loggable, BalConfigObserver { private animationItem!: AnimationItem private animationFunction?: SpinnerAnimationFunction private currentRaf: number | undefined log!: LogInstance + @State() animated = defaultConfig.animated + @Logger('bal-spinner') createLogger(log: LogInstance) { this.log = log @@ -52,20 +56,39 @@ export class Spinner implements ComponentInterface, Loggable { /** * Defines the color of the spinner. */ - @Prop() color: 'blue' | 'white' = 'blue' + @Prop() color: BalProps.BalSpinnerColor = 'blue' /** * If `true` the component is smaller */ @Prop() small = false + /** + * Defines the look of the spinner + */ + @Prop() variation: BalProps.BalSpinnerVariation = 'logo' + @Watch('variation') + variationWatcher(newValue: BalProps.BalSpinnerVariation, oldValue: BalProps.BalSpinnerVariation) { + if (newValue !== oldValue) { + if (this.variation === 'circle') { + this.destroy() + } else { + this.animate() + } + } + } + /** * LIFECYCLE * ------------------------------------------------------ */ componentDidLoad() { - this.animate() + if (this.variation === 'logo') { + this.animate() + } else { + this.destroy() + } } disconnectedCallback() { @@ -74,12 +97,30 @@ export class Spinner implements ComponentInterface, Loggable { } } + /** + * LISTENERS + * ------------------------------------------------------ + */ + + @ListenToConfig() + configChanged(state: BalConfigState): void { + this.animated = state.animated + if (state.animated === false) { + this.destroy() + } + } + /** * PRIVATE METHODS * ------------------------------------------------------ */ private animate = async () => { + if (!this.animated) { + this.destroy() + return + } + await this.load() if (this.currentRaf !== undefined) { @@ -104,6 +145,10 @@ export class Spinner implements ComponentInterface, Loggable { } private shouldAnimate = () => { + if (this.variation !== 'logo') { + return false + } + if (typeof (window as any) === 'undefined') { return false } @@ -147,6 +192,19 @@ export class Spinner implements ComponentInterface, Loggable { */ render() { - return + const block = BEM.block('spinner') + + return ( + + ) } } diff --git a/packages/core/src/components/bal-spinner/test/bal-spinner.cy.html b/packages/core/src/components/bal-spinner/test/bal-spinner.cy.html index fd6f97ad75..da45dd65f7 100644 --- a/packages/core/src/components/bal-spinner/test/bal-spinner.cy.html +++ b/packages/core/src/components/bal-spinner/test/bal-spinner.cy.html @@ -23,10 +23,16 @@

    Small

    Inverted

    -
    +
    + +

    Circle Variation

    +
    + + +
    diff --git a/packages/core/src/components/bal-tag/bal-tag.sass b/packages/core/src/components/bal-tag/bal-tag.sass index 2eedce0e5e..4193c6d690 100644 --- a/packages/core/src/components/bal-tag/bal-tag.sass +++ b/packages/core/src/components/bal-tag/bal-tag.sass @@ -68,8 +68,8 @@ gap: .25rem // sizes +modifier(is-small) - font-size: var(--bal-text-size-small) !important - font-weight: var(--bal-font-weight-regular) !important + font-size: var(--bal-tag-size-small-font-size) !important + font-weight: var(--bal-tag-size-small-font-weight) !important +modifier(is-medium) font-size: var(--bal-text-size-medium) !important +modifier(is-large) diff --git a/packages/core/src/components/bal-tag/bal-tag.vars.sass b/packages/core/src/components/bal-tag/bal-tag.vars.sass index 54cbeb19c7..443f9bbab3 100644 --- a/packages/core/src/components/bal-tag/bal-tag.vars.sass +++ b/packages/core/src/components/bal-tag/bal-tag.vars.sass @@ -85,3 +85,7 @@ --bal-tag-text-green-light: var(--bal-color-primary) --bal-tag-text-invalid: var(--bal-color-text-white) --bal-tag-text-disabled: var(--bal-color-text-white) + // + // size small + --bal-tag-size-small-font-size: var(--bal-text-size-small) + --bal-tag-size-small-font-weight: var(--bal-font-weight-regular) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 26e090809e..0139383f6c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -57,6 +57,7 @@ export * from './components/bal-label/bal-label.i18n' * Utils */ export { initializeBaloiseDesignSystem as initialize, initializeBaloiseDesignSystem } from './initialize' +export { newBalOption } from './utils/dropdown/option' export { newBalStepOption } from './components/bal-steps/bal-step.util' export { newBalTabOption } from './components/bal-tabs/bal-tab.util' export { newBalCheckboxOption } from './components/bal-checkbox/utils/bal-checkbox.util' diff --git a/packages/core/src/interfaces.d.ts b/packages/core/src/interfaces.d.ts index 8b221bbaf6..ca5b29c43b 100644 --- a/packages/core/src/interfaces.d.ts +++ b/packages/core/src/interfaces.d.ts @@ -13,6 +13,7 @@ import './components/bal-card/bal-card.interfaces' import './components/bal-carousel/bal-carousel.interfaces' import './components/bal-close/bal-close.interfaces' import './components/bal-data/bal-data.interfaces' +import './components/bal-dropdown/bal-dropdown.interfaces' import './components/bal-icon/bal-icon.interfaces' import './components/bal-list/bal-list.interfaces' import './components/bal-logo/bal-logo.interfaces' @@ -24,6 +25,8 @@ import './components/bal-nav/bal-nav-menu-bar/bal-nav-menu-bar.interfaces' import './components/bal-nav/bal-nav-menu-flyout/bal-nav-menu-flyout.interfaces' import './components/bal-nav/bal-nav.interfaces' import './components/bal-navbar/bal-navbar.interfaces' +import './components/bal-option/bal-option.interfaces' +import './components/bal-option-list/bal-option-list.interfaces' import './components/bal-pagination/bal-pagination.interfaces' import './components/bal-popover/bal-popover.interfaces' import './components/bal-popup/bal-popup.interfaces' @@ -40,6 +43,7 @@ import './components/bal-text/bal-text.interfaces' import './components/bal-content/bal-content.interfaces' import './components/bal-divider/bal-divider.interfaces' import './components/bal-stack/bal-stack.interfaces' +import './components/bal-spinner/bal-spinner.interfaces' import './components/bal-checkbox/bal-checkbox.interfaces' import './components/bal-date/bal-date.interfaces' import './components/bal-date/bal-date-calendar/bal-date-calendar.interfaces' diff --git a/packages/core/src/utils/aria.ts b/packages/core/src/utils/aria.ts new file mode 100644 index 0000000000..961c2e6c75 --- /dev/null +++ b/packages/core/src/utils/aria.ts @@ -0,0 +1 @@ +export const ariaBooleanToString = (bool?: boolean) => (!!bool ? 'true' : 'false') diff --git a/packages/core/src/utils/config/config.default.ts b/packages/core/src/utils/config/config.default.ts index 97c5228db3..6a1a01caad 100644 --- a/packages/core/src/utils/config/config.default.ts +++ b/packages/core/src/utils/config/config.default.ts @@ -68,6 +68,7 @@ export const defaultConfig: BalConfigState = { fallbackLanguage: 'de', logger: defaultLoggerConfig, animated: true, + httpFormSubmit: true, } export const defaultLocale = `${defaultConfig.language}-${defaultConfig.region}` diff --git a/packages/core/src/utils/config/config.ts b/packages/core/src/utils/config/config.ts index e955de6a7e..8e35d6073a 100644 --- a/packages/core/src/utils/config/config.ts +++ b/packages/core/src/utils/config/config.ts @@ -88,6 +88,15 @@ export class Config { this._notify() } + get httpFormSubmit(): boolean { + return this._config.httpFormSubmit + } + + set httpFormSubmit(httpFormSubmit: boolean) { + this._config.httpFormSubmit = httpFormSubmit + this._notify() + } + attach(observer: BalConfigObserver): void { const isExist = this._observers.includes(observer) if (isExist) { diff --git a/packages/core/src/utils/config/config.types.ts b/packages/core/src/utils/config/config.types.ts index a864eeca9d..006073b617 100644 --- a/packages/core/src/utils/config/config.types.ts +++ b/packages/core/src/utils/config/config.types.ts @@ -25,6 +25,7 @@ export interface BalConfig { fallbackLanguage?: BalLanguage logger?: BalLogger animated?: boolean + httpFormSubmit?: boolean _jmp?: (c: any) => any _raf?: (c: any) => number _ael?: (el: any, eventName: string, listener: any, options: any) => void @@ -40,6 +41,7 @@ export interface BalConfigState { fallbackLanguage: BalLanguage logger: BalLogger animated: boolean + httpFormSubmit: boolean } export interface BalPlatformConfig { diff --git a/packages/core/src/utils/dropdown/auto-fill.ts b/packages/core/src/utils/dropdown/auto-fill.ts new file mode 100644 index 0000000000..13471116c9 --- /dev/null +++ b/packages/core/src/utils/dropdown/auto-fill.ts @@ -0,0 +1,47 @@ +import { areArraysEqual } from '@baloise/web-app-utils' +import { stopEventBubbling } from '../form-input' +import { DropdownComponent } from './component' + +export class DropdownAutoFillUtil { + private component!: DropdownComponent + + connectedCallback(component: DropdownComponent) { + this.component = component + } + + handleAutoFill = async (ev: Event) => { + stopEventBubbling(ev) + if (this.isAutoFillAllowed()) { + this.component.isAutoFilled = true + + const autoFillValue = this.component.nativeEl.value + const newValue = await this.parseAutoFillValueWithOptions(autoFillValue) + if (newValue === undefined) { + this.component.isAutoFilled = false + return + } + + if (!areArraysEqual(newValue, this.component.rawValue)) { + this.component.valueUtil.updateRawValueBySelection(newValue, true) + } + } + } + + private isAutoFillAllowed(): boolean { + return !this.component.multiple + } + + private async parseAutoFillValueWithOptions(autoFillValue: string): Promise { + const options = await this.component.listEl.getOptions() + const value = undefined + + for (let index = 0; index < options.length; index++) { + const option = options[index] + if (option.value === autoFillValue || option.label === autoFillValue) { + return [option.value] + } + } + + return value + } +} diff --git a/packages/core/src/utils/dropdown/component.ts b/packages/core/src/utils/dropdown/component.ts new file mode 100644 index 0000000000..b8394a2069 --- /dev/null +++ b/packages/core/src/utils/dropdown/component.ts @@ -0,0 +1,39 @@ +import { EventEmitter } from '@stencil/core' +import { DropdownValueUtil } from './value' +import { BalOption } from './option' +import { DropdownFocus } from './focus' +import { DropdownPopupUtil } from './popup' + +export type DropdownComponent = DropdownFocus & { + el: HTMLElement + selectEl: HTMLSelectElement | undefined + panelEl: HTMLDivElement | undefined + nativeEl: HTMLInputElement | undefined + listEl: HTMLBalOptionListElement | undefined + + valueUtil: DropdownValueUtil + popupUtil: DropdownPopupUtil + + multiple: boolean + chips: boolean + readonly: boolean + disabled: boolean + clearable: boolean + hasFocus: boolean + isExpanded: boolean + isAutoFilled: boolean + inputLabel: string + + nativeOptions: string[] + choices: BalOption[] + options: BalOption[] + rawOptions: BalOption[] + rawValue: string[] + value?: string | string[] + initialValue?: string | string[] + + panelCleanup?: () => void + balChange: EventEmitter + balBlur: EventEmitter + balFocus: EventEmitter +} diff --git a/packages/core/src/utils/dropdown/dropdown.i18n.ts b/packages/core/src/utils/dropdown/dropdown.i18n.ts new file mode 100644 index 0000000000..3b88a11802 --- /dev/null +++ b/packages/core/src/utils/dropdown/dropdown.i18n.ts @@ -0,0 +1,60 @@ +import { I18n } from '../../interfaces' + +interface I18nBalDropdown { + clearable: string + open: string + close: string +} + +export const i18nBalDropdown: I18n = { + de: { + clearable: 'Löschen', + open: 'Öffnen', + close: 'Schließen', + }, + en: { + clearable: 'clear', + open: 'Open', + close: 'Close', + }, + fr: { + clearable: 'Effacer', + open: 'Ouvrir', + close: 'Fermer', + }, + it: { + clearable: 'Cancellare', + open: 'Apri', + close: 'Chiudi', + }, + nl: { + clearable: 'Wissen', + open: 'Open', + close: 'Sluiten', + }, + es: { + clearable: 'Limpiar', + open: 'Abrir', + close: 'Cerrar', + }, + pl: { + clearable: 'Wyczyść', + open: 'Otwórz', + close: 'Zamknij', + }, + pt: { + clearable: 'Limpar', + open: 'Abrir', + close: 'Fechar', + }, + sv: { + clearable: 'Rensa', + open: 'Öppna', + close: 'Stäng', + }, + fi: { + clearable: 'Tyhjennä', + open: 'Avaa', + close: 'Sulje', + }, +} diff --git a/packages/core/src/utils/dropdown/events.ts b/packages/core/src/utils/dropdown/events.ts new file mode 100644 index 0000000000..8afaf84f2b --- /dev/null +++ b/packages/core/src/utils/dropdown/events.ts @@ -0,0 +1,57 @@ +import { DropdownComponent } from './component' + +export type DropdownEvents = { + listenOnClickOutside(ev: UIEvent) + listenOnClick(ev: UIEvent) +} + +export class DropdownEventsUtil { + private component!: DropdownComponent + + connectedCallback(component: DropdownComponent) { + this.component = component + } + + handleFocus(ev: FocusEvent) { + this.component.hasFocus = true + this.component.balFocus.emit(ev) + } + + handleBlur(ev: FocusEvent) { + this.component.hasFocus = false + this.component.balBlur.emit(ev) + } + + handleClick(ev: MouseEvent) { + if (!this.component.valueUtil.isDisabled()) { + if (this.component.chips) { + const targetEl = ev.target as HTMLElement + const closeEl = targetEl.closest('bal-close') + if (closeEl) { + return + } + } + + if (this.component.clearable) { + const targetEl = ev.target as HTMLElement + const clearEl = targetEl.closest('.bal-dropdown__clear') + if (clearEl) { + this.component.valueUtil.updateRawValueBySelection([]) + return + } + } + + this.component.popupUtil.toggleList() + } + } + + // @Listen('click', { target: 'document' }) + handleOutsideClick(ev: UIEvent) { + if (this.component.isExpanded) { + if (!this.component.el.contains(ev.target as Node)) { + this.component.isExpanded = false + this.component.listEl?.resetFocus() + } + } + } +} diff --git a/packages/core/src/utils/dropdown/filters.ts b/packages/core/src/utils/dropdown/filters.ts new file mode 100644 index 0000000000..53b593f0b6 --- /dev/null +++ b/packages/core/src/utils/dropdown/filters.ts @@ -0,0 +1,11 @@ +export function startsWith(text: string, input: string): boolean { + const content = text.trim().toLowerCase() + const value = input.trim().toLowerCase() + return content.startsWith(value) +} + +export function includes(text: string, input: string): boolean { + const content = text.trim().toLowerCase() + const value = input.trim().toLowerCase() + return content.includes(value) +} diff --git a/packages/core/src/utils/dropdown/focus.ts b/packages/core/src/utils/dropdown/focus.ts new file mode 100644 index 0000000000..8fef4fd00e --- /dev/null +++ b/packages/core/src/utils/dropdown/focus.ts @@ -0,0 +1,31 @@ +import { DropdownComponent } from './component' + +export type DropdownFocus = { + labelToFocus: string +} + +export class DropdownFocusUtil { + private component!: DropdownComponent + + private keyHitTimeout!: NodeJS.Timeout + private timeout!: NodeJS.Timeout + + connectedCallback(component: DropdownComponent) { + this.component = component + } + + public focusOptionByLabel(key: string, options: Partial<{ select: boolean }> = {}) { + this.component.labelToFocus = (this.component.labelToFocus + key).trim() + if (this.component.labelToFocus.length > 0) { + clearTimeout(this.keyHitTimeout) + this.keyHitTimeout = setTimeout(() => { + this.component.listEl?.focusByLabel(this.component.labelToFocus, options) + }, 100) + + clearTimeout(this.timeout) + this.timeout = setTimeout(async () => { + this.component.labelToFocus = '' + }, 1000) + } + } +} diff --git a/packages/core/src/utils/dropdown/form-submit.tsx b/packages/core/src/utils/dropdown/form-submit.tsx new file mode 100644 index 0000000000..39a52eae70 --- /dev/null +++ b/packages/core/src/utils/dropdown/form-submit.tsx @@ -0,0 +1,88 @@ +import { FunctionalComponent, h } from '@stencil/core' +import { DropdownComponent } from './component' +import { BEM } from '../bem' + +export type DropdownFormSubmit = { + resetHandler(ev: UIEvent) +} + +export class DropdownFormSubmitUtil { + private component!: DropdownComponent + private resetHandlerTimer?: NodeJS.Timer + + connectedCallback(component: DropdownComponent) { + this.component = component + this.component.initialValue = this.component.value + } + + componentDidRender() { + if (this.component.selectEl) { + const options = this.component.selectEl.querySelectorAll('option') + options.forEach(option => { + if (this.component.rawValue.includes(option.value)) { + option.selected = true + } + }) + } + } + + // @Listen('reset', { capture: true, target: 'document' }) + handle(ev: UIEvent) { + const formElement = ev.target as HTMLElement + if (formElement?.contains(this.component.el)) { + if (this.resetHandlerTimer) { + clearTimeout(this.resetHandlerTimer) + } + + this.resetHandlerTimer = setTimeout(() => { + const newRawValue = this.component.valueUtil.parseValueString(this.component.initialValue) + this.component.valueUtil.updateRawValueBySelection(newRawValue) + }, 0) + } + } +} + +export interface DropdownNativeSelectProps { + name: string + httpFormSubmit: boolean + multiple: boolean + required: boolean + disabled: boolean + rawValue: string[] + refSelectEl: (el: HTMLSelectElement) => void +} + +export const DropdownNativeSelect: FunctionalComponent = ({ + name, + httpFormSubmit, + multiple, + required, + disabled, + rawValue, + refSelectEl, +}) => { + const block = BEM.block('dropdown') + + return httpFormSubmit ? ( + + ) : ( + '' + ) +} diff --git a/packages/core/src/utils/dropdown/icon.tsx b/packages/core/src/utils/dropdown/icon.tsx new file mode 100644 index 0000000000..34f25d1dc3 --- /dev/null +++ b/packages/core/src/utils/dropdown/icon.tsx @@ -0,0 +1,54 @@ +import { FunctionalComponent, h } from '@stencil/core' +import { BalLanguage } from '../config' +import { BEM } from '../bem' +import { i18nBalDropdown } from './dropdown.i18n' + +export interface DropdownIconProps { + language: BalLanguage + loading: boolean + clearable: boolean + filled: boolean + disabled: boolean + invalid: boolean + expanded: boolean + icon: string +} + +export const DropdownIcon: FunctionalComponent = ({ + icon, + language, + loading, + clearable, + invalid, + filled, + expanded, + disabled, +}) => { + const block = BEM.block('dropdown') + + if (loading) { + return + } else if (clearable && filled && !disabled) { + return ( + + ) + } else { + return ( + + ) + } +} diff --git a/packages/core/src/utils/dropdown/index.ts b/packages/core/src/utils/dropdown/index.ts new file mode 100644 index 0000000000..e87e926f5c --- /dev/null +++ b/packages/core/src/utils/dropdown/index.ts @@ -0,0 +1,13 @@ +export * from './auto-fill' +export * from './component' +export * from './dropdown.i18n' +export * from './events' +export * from './filters' +export * from './focus' +export * from './form-submit' +export * from './icon' +export * from './option' +export * from './option-list' +export * from './popup' +export * from './value' +export * from './input' diff --git a/packages/core/src/utils/dropdown/input.tsx b/packages/core/src/utils/dropdown/input.tsx new file mode 100644 index 0000000000..b80b8c5411 --- /dev/null +++ b/packages/core/src/utils/dropdown/input.tsx @@ -0,0 +1,102 @@ +import { h, FunctionalComponent } from '@stencil/core' +import { BEM } from '../bem' +import { i18nBalDropdown } from './dropdown.i18n' +import { ariaBooleanToString } from '../aria' +import { BalAriaForm } from '../form' +import { BalLanguage } from '../config' +import { Attributes } from '../attributes' + +export interface DropdownInputProps { + name: string + inputId: string + httpFormSubmit: boolean + ariaForm: BalAriaForm + rawValue: string[] + autocomplete: string + placeholder: string + inputLabel: string + required: boolean + disabled: boolean + readonly: boolean + expanded: boolean + invalid: boolean + language: BalLanguage + inheritedAttributes: Attributes + refInputEl: (el: HTMLInputElement) => void + onChange: (ev: Event) => void + onFocus: (ev: FocusEvent) => void + onBlur: (ev: FocusEvent) => void + onKeyDown: (ev: KeyboardEvent) => void +} + +export const DropdownInput: FunctionalComponent = ({ + name, + inputId, + httpFormSubmit, + ariaForm, + rawValue, + autocomplete, + required, + disabled, + readonly, + placeholder, + expanded, + invalid, + language, + inputLabel, + inheritedAttributes, + refInputEl, + onChange, + onFocus, + onBlur, + onKeyDown, +}) => { + const block = BEM.block('dropdown') + + const Input = () => ( + refInputEl(el)} + onChange={ev => onChange(ev)} + onFocus={ev => onFocus(ev)} + onBlur={ev => onBlur(ev)} + onKeyDown={ev => onKeyDown(ev)} + {...inheritedAttributes} + /> + ) + + if (httpFormSubmit) { + return + } + + return ( +
    + +
    + ) +} diff --git a/packages/core/src/utils/dropdown/option-list.tsx b/packages/core/src/utils/dropdown/option-list.tsx new file mode 100644 index 0000000000..ce65cb6f10 --- /dev/null +++ b/packages/core/src/utils/dropdown/option-list.tsx @@ -0,0 +1,73 @@ +import { FunctionalComponent, h } from '@stencil/core' +import { BalOption } from './option' +import { BEM } from '../bem' + +export interface DropdownOptionListProps { + inputId: string + block: string + filter: BalProps.BalOptionListFilter + required: boolean + isExpanded: boolean + isDisabled: boolean + hasPropOptions: boolean + multiple: boolean + contentHeight: number + rawOptions: BalOption[] + refPanelEl: (el: HTMLDivElement) => void + refListEl: (el: HTMLBalOptionListElement) => void +} + +export const DropdownOptionList: FunctionalComponent = ({ + inputId, + isExpanded, + rawOptions, + isDisabled, + hasPropOptions, + required, + filter, + multiple, + contentHeight, + refPanelEl, + refListEl, +}) => { + const block = BEM.block('dropdown') + + return ( +
    + + + {hasPropOptions + ? rawOptions.map(option => ( + + )) + : ''} + +
    + ) +} diff --git a/packages/core/src/utils/dropdown/option.tsx b/packages/core/src/utils/dropdown/option.tsx new file mode 100644 index 0000000000..99433a3c74 --- /dev/null +++ b/packages/core/src/utils/dropdown/option.tsx @@ -0,0 +1,90 @@ +import { h } from '@stencil/core' +import { DropdownComponent } from './component' + +export type BalBaseOption = { + value: TValue + label: string +} + +export type BalOptionOptions = { + // state values + disabled: boolean + invalid: boolean + selected: boolean + focused: boolean + hidden: boolean + // visual values + multiline: boolean +} + +export type BalOption = BalBaseOption & BalOptionOptions + +export type NewBalOption = ( + option: BalBaseOption, + options?: Partial, +) => BalOption + +export const newBalOption: NewBalOption = (option, options) => { + const data: BalOption = { + ...option, + disabled: false, + invalid: false, + selected: false, + focused: false, + hidden: false, + multiline: false, + } + + if (options) { + data.disabled = options.disabled === undefined ? data.disabled : options.disabled + data.invalid = options.invalid === undefined ? data.invalid : options.invalid + data.selected = options.selected === undefined ? data.selected : options.selected + data.focused = options.focused === undefined ? data.focused : options.focused + data.hidden = options.hidden === undefined ? data.hidden : options.hidden + data.multiline = options.multiline === undefined ? data.multiline : options.multiline + } + + return data +} + +export const mapOption = (option: Partial): BalOption => { + return newBalOption( + { + value: option.value, + label: option.label, + }, + option, + ) +} + +export class DropdownOptionUtil { + private component!: DropdownComponent + + connectedCallback(component: DropdownComponent) { + this.component = component + this.optionChanged() + } + + async componentWillRender() { + if (this.component.listEl) { + this.component.nativeOptions = await this.component.listEl.getValues() + } + } + + hasPropOptions(): boolean { + return this.component.options && this.component.options.length > 0 + } + + async optionChanged() { + this.component.rawOptions = this.component.options.map(mapOption) + await this.component.valueUtil.updateInputContent() + } + + async listenToOptionChange(_ev: BalEvents.BalOptionChange) { + const newSelectedValues = (await this.component.listEl?.getSelectedValues()) || [] + this.component.valueUtil.updateRawValueBySelection(newSelectedValues) + if (!this.component.multiple) { + this.component.popupUtil.collapseList() + } + } +} diff --git a/packages/core/src/utils/dropdown/popup.ts b/packages/core/src/utils/dropdown/popup.ts new file mode 100644 index 0000000000..fd146f1199 --- /dev/null +++ b/packages/core/src/utils/dropdown/popup.ts @@ -0,0 +1,52 @@ +import { autoUpdate, computePosition, flip, shift } from '@floating-ui/dom' +import { DropdownComponent } from './component' + +export class DropdownPopupUtil { + private component!: DropdownComponent + + connectedCallback(component: DropdownComponent) { + this.component = component + } + + updatePanelPosition = (referenceEl: HTMLElement, floatingEl: HTMLElement) => () => { + computePosition(referenceEl, floatingEl, { + placement: 'bottom-start', + middleware: [flip(), shift()], + }).then(({ x, y }) => { + Object.assign(floatingEl.style, { + left: `${x}px`, + top: `${y}px`, + }) + }) + } + + toggleList() { + if (!this.component.valueUtil.isDisabled()) { + if (this.component.isExpanded) { + this.collapseList() + } else { + this.expandList() + } + } + } + + expandList() { + if (this.component.panelEl) { + this.component.panelCleanup = autoUpdate( + this.component.el, + this.component.panelEl, + this.updatePanelPosition(this.component.el, this.component.panelEl), + ) + } + this.component.isExpanded = true + this.component.listEl?.focusFirst() + } + + collapseList() { + this.component.isExpanded = false + this.component.listEl?.resetFocus() + if (this.component.panelCleanup) { + this.component.panelCleanup() + } + } +} diff --git a/packages/core/src/utils/dropdown/value.tsx b/packages/core/src/utils/dropdown/value.tsx new file mode 100644 index 0000000000..374d3d1099 --- /dev/null +++ b/packages/core/src/utils/dropdown/value.tsx @@ -0,0 +1,157 @@ +import { FunctionalComponent, h } from '@stencil/core' +import { areArraysEqual } from '@baloise/web-app-utils' +import isNil from 'lodash.isnil' +import { DropdownComponent } from './component' +import { BEM } from '../bem' +import { BalOption } from './option' +import { waitAfterFramePaint } from '../helpers' + +export class DropdownValueUtil { + private component!: DropdownComponent + + connectedCallback(component: DropdownComponent) { + this.component = component + } + + componentDidLoad(): void { + setTimeout(() => this.valueChanged(this.component.value, undefined), 0) + } + + isDisabled(): boolean { + return this.component.disabled || this.component.readonly + } + + isFilled(): boolean { + return this.component.rawValue && this.component.rawValue.length > 0 + } + + valueChanged(newValue: string | string[] | undefined, oldValue: string | string[] | undefined) { + const newValueType = typeof newValue + const oldValueType = typeof oldValue + + if (newValueType !== oldValueType) { + this.updateRawValueByValueProp(newValue) + } + + if (newValueType === 'string' && newValue !== oldValue) { + this.updateRawValueByValueProp(newValue) + } + + if (Array.isArray(newValue) && Array.isArray(oldValue) && !areArraysEqual(newValue, oldValue)) { + this.updateRawValueByValueProp(newValue) + } + } + + updateRawValueBySelection(newRawValue: string[] = [], isAutoFilled = false) { + this.component.isAutoFilled = isAutoFilled + this.updateRawValue(newRawValue) + if (this.component.multiple) { + this.component.balChange.emit(this.component.rawValue) + } else { + this.component.balChange.emit(this.component.rawValue[0]) + } + } + + parseValueString(newValue: string | string[] = []) { + let newRawValue: string[] = [] + + if (!isNil(newValue) && newValue !== '') { + if (Array.isArray(newValue)) { + newRawValue = [...newValue.filter(v => !isNil(v))] + } else { + if (newValue.split('').includes(',')) { + newRawValue = [ + ...newValue + .split(',') + .filter(v => v) + .map(v => v.trim()), + ] + } else { + newRawValue = [newValue] + } + } + } + return newRawValue + } + + updateRawValueByValueProp(newValue: string | string[] = []) { + const newRawValue = this.parseValueString(newValue) + this.updateRawValue(newRawValue) + } + + async updateRawValue(newRawValue: string[] = []) { + this.component.rawValue = newRawValue + + if (this.component.listEl) { + await this.component.listEl.updateSelected(this.component.rawValue) + } + + await this.updateInputContent() + } + + removeOption(option: BalOption) { + const newRawValue = this.component.rawValue.filter(value => value !== option.value) + this.updateRawValueBySelection(newRawValue) + } + + async updateInputContent() { + await waitAfterFramePaint() + if (this.component.listEl) { + this.component.choices = await this.component.listEl.getSelectedOptions(this.component.rawValue) + this.component.inputLabel = this.component.choices + .map(option => option.label.trim()) + .sort() + .join(',') + } + } +} + +export interface DropdownValueProps { + filled: boolean + chips: boolean + invalid: boolean + disabled: boolean + readonly: boolean + placeholder: string + choices: BalOption[] + onRemoveChip: (option: BalOption) => void +} + +export const DropdownValue: FunctionalComponent = ({ + filled, + chips, + placeholder, + choices, + invalid, + disabled, + readonly, + onRemoveChip, +}) => { + const block = BEM.block('dropdown') + + if (filled) { + if (chips) { + return ( +
    + {choices.map(option => ( + onRemoveChip(option)} + > + {option.label} + + ))} +
    + ) + } else { + return choices.map(option => option.label).join(', ') + } + } else { + return placeholder + } +} diff --git a/packages/core/stencil.config.ts b/packages/core/stencil.config.ts index 80a7cbc6e6..5f97658829 100644 --- a/packages/core/stencil.config.ts +++ b/packages/core/stencil.config.ts @@ -217,6 +217,7 @@ export const config: Config = { // form components { components: ['bal-checkbox', 'bal-checkbox-group'] }, { components: ['bal-datepicker'] }, + { components: ['bal-dropdown'] }, { components: ['bal-field', 'bal-field-label', 'bal-field-control', 'bal-field-message', 'bal-field-hint'] }, { components: ['bal-file-upload'] }, { components: ['bal-form'] }, @@ -230,6 +231,9 @@ export const config: Config = { { components: ['bal-select', 'bal-select-option'] }, { components: ['bal-textarea'] }, { components: ['bal-time-input'] }, + { + components: ['bal-option-list', 'bal-option'], + }, // // overlay components { components: ['bal-modal', 'bal-modal-body', 'bal-modal-header'] }, diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 3ff09abbbe..557ccdc4b1 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -6,7 +6,10 @@ interface BaloiseDesignSystemReactConfig { } export const useBaloiseDesignSystem = (config: BaloiseDesignSystemReactConfig = {}) => { - initialize(config.defaults) + initialize({ + ...config.defaults, + httpFormSubmit: false, + }) } export * from './generated/proxies' diff --git a/packages/testing/src/commands/custom/bal-select.command.ts b/packages/testing/src/commands/custom/bal-select.command.ts index a0b848a1d9..cebca81a8e 100644 --- a/packages/testing/src/commands/custom/bal-select.command.ts +++ b/packages/testing/src/commands/custom/bal-select.command.ts @@ -1,4 +1,4 @@ -import { log, wrapOptions } from '../helpers' +import { isDropDown, log, wrapOptions } from '../helpers' import { selectors } from '../../selectors' Cypress.Commands.add( @@ -8,14 +8,20 @@ Cypress.Commands.add( }, (subject, options) => { const o = wrapOptions(options) - return cy - .wrapComponent(subject, o) - .find(selectors.select.options, o) - .then($el => { - log('balSelectFindOptions', '', $el, options) - return $el - }) - .waitForComponents(o) + + if (isDropDown(subject)) { + return cy + .wrapComponent(subject, o) + .find(selectors.dropdown.options, o) + .then($el => log('balSelectFindOptions', '', $el, options)) + .waitForComponents(o) + } else { + return cy + .wrapComponent(subject, o) + .find(selectors.select.options, o) + .then($el => log('balSelectFindOptions', '', $el, options)) + .waitForComponents(o) + } }, ) @@ -25,8 +31,9 @@ Cypress.Commands.add( prevSubject: true, }, (subject, labels, dataKey = 'label', options) => { - log('balAccordionIsOpen', '', subject, options) + log('balSelectShouldHaveOptions', '', subject, options) const o = wrapOptions(options) + return cy .wrapComponent(subject, o) .balSelectFindOptions(o) @@ -44,13 +51,12 @@ Cypress.Commands.add( }, (subject, options) => { const o = wrapOptions(options) + const selector = isDropDown(subject) ? selectors.dropdown.chips : selectors.select.chips + return cy .wrapComponent(subject, o) - .find(selectors.select.chips, o) - .then($el => { - log('balSelectFindChips', '', $el, options) - return $el - }) + .find(selector, o) + .then($el => log('balSelectFindChips', '', $el, options)) .waitForComponents(o) }, ) diff --git a/packages/testing/src/commands/custom/get.command.ts b/packages/testing/src/commands/custom/get.command.ts index 1aeac790b3..3f27a784fc 100644 --- a/packages/testing/src/commands/custom/get.command.ts +++ b/packages/testing/src/commands/custom/get.command.ts @@ -47,6 +47,7 @@ Cypress.Commands.add('getByLabelText', { prevSubject: ['optional'] }, (subject, o, ) }) + .first(o) .then(o, $el => log(!!subject ? '-getByLabelText' : 'getByLabelText', labelText, $el, options)) as any } else { return cy @@ -58,6 +59,7 @@ Cypress.Commands.add('getByLabelText', { prevSubject: ['optional'] }, (subject, o, ) }) + .first(o) .then(o, $el => log(!!subject ? '-getByLabelText' : 'getByLabelText', labelText, $el, options)) as any } }) @@ -70,20 +72,10 @@ Cypress.Commands.add( (subject, placeholder, options?: Partial): any => { const o = wrapOptions(options) + const selector = `input[placeholder="${placeholder}"], textarea[placeholder="${placeholder}"], [data-placeholder="${placeholder}"]` const element = subject - ? cy - .wrap(subject, o) - .find( - `input[placeholder="${placeholder}"], textarea[placeholder="${placeholder}"], [data-placeholder="${placeholder}"]`, - o, - ) - .waitForComponents(o) - : cy - .get( - `input[placeholder="${placeholder}"], textarea[placeholder="${placeholder}"], [data-placeholder="${placeholder}"]`, - o, - ) - .waitForComponents(o) + ? cy.wrap(subject, o).find(selector, o).first(o).waitForComponents(o) + : cy.get(selector, o).first(o).waitForComponents(o) element.then(o, $el => log(!!subject ? '-getByPlaceholder' : 'getByPlaceholder', placeholder, $el, options)) return element @@ -104,7 +96,7 @@ Cypress.Commands.add( function filterVisibleElements(elements: HTMLElement[]) { return elements.filter(element => { - const isElementAriaHidden = options.hidden === true ? false : !!Cypress.$(element).attr('aria-hidden') + const isElementAriaHidden = options.hidden === true ? false : Cypress.$(element).attr('aria-hidden') === 'true' return !isElementAriaHidden }) } diff --git a/packages/testing/src/commands/helpers.ts b/packages/testing/src/commands/helpers.ts index 1e6472cd58..858d4ed3aa 100644 --- a/packages/testing/src/commands/helpers.ts +++ b/packages/testing/src/commands/helpers.ts @@ -86,6 +86,7 @@ export const isSelect: isElementType = el => isElement(el, 'BAL-SELECT') export const isTag: isElementType = el => isElement(el, 'BAL-TAG') export const isTabs: isElementType = el => isElement(el, 'BAL-TABS') export const isSteps: isElementType = el => isElement(el, 'BAL-STEPS') +export const isDropDown: isElementType = el => isElement(el, 'BAL-DROPDOWN') export const isSlider: isElementType = el => isElement(el, 'BAL-INPUT-SLIDER') export const isHint: isElementType = el => isElement(el, 'BAL-HINT') export const isTextarea: isElementType = el => isElement(el, 'BAL-TEXTAREA') diff --git a/packages/testing/src/commands/overrides/blur.command.ts b/packages/testing/src/commands/overrides/blur.command.ts index 15fa908f35..f77ead82c8 100644 --- a/packages/testing/src/commands/overrides/blur.command.ts +++ b/packages/testing/src/commands/overrides/blur.command.ts @@ -13,6 +13,7 @@ import { wrapOptions, wrapCommand, isInputDate, + isDropDown, } from '../helpers' Cypress.Commands.overwrite('blur', (originalFn: any, element: Cypress.Chainable, options: any) => { @@ -62,5 +63,9 @@ Cypress.Commands.overwrite('blur', (originalFn: any, element: Cypress. return command(selectors.select.input) } + if (isDropDown(element)) { + return command(selectors.dropdown.input) + } + return originalFn(element, options) }) diff --git a/packages/testing/src/commands/overrides/clear.command.ts b/packages/testing/src/commands/overrides/clear.command.ts index 2abc1a49f0..0898ba35e6 100644 --- a/packages/testing/src/commands/overrides/clear.command.ts +++ b/packages/testing/src/commands/overrides/clear.command.ts @@ -10,6 +10,7 @@ import { wrapCommand, wrapOptions, isInputDate, + isDropDown, } from '../helpers' import { selectors } from '../../selectors' @@ -52,5 +53,9 @@ Cypress.Commands.overwrite('clear', (originalFn: any, element: Cypress return command('.bal-select__control__input') } + if (isDropDown(element)) { + return command(selectors.dropdown.input) + } + return originalFn(element, options) }) diff --git a/packages/testing/src/commands/overrides/click.command.ts b/packages/testing/src/commands/overrides/click.command.ts index feaed7f951..c9e1f8e3e0 100644 --- a/packages/testing/src/commands/overrides/click.command.ts +++ b/packages/testing/src/commands/overrides/click.command.ts @@ -9,11 +9,21 @@ import { isHint, wrapCommand, wrapOptions, + isDropDown, + log, } from '../helpers' import { selectors } from '../../selectors' Cypress.Commands.overwrite('click', (originalFn: any, element: Cypress.Chainable, options) => { - const command = wrapCommand('click', element, '', $el => originalFn($el, wrapOptions(options))) + const command = (selector: string) => { + return cy + .wrapComponent(element as any, { log: false }) + .waitForComponents({ log: false }) + .find(selector, { log: false }) + .click({ force: true, log: false }) + .then($el => log('click', '', $el)) + .wrapComponent(element as any, { log: false }) + } if (isAccordion(element)) { return command(selectors.accordion.trigger) @@ -35,13 +45,17 @@ Cypress.Commands.overwrite('click', (originalFn: any, element: Cypress return command(selectors.radio.label) } - if (isTag(element) && hasClass(element, 'sc-bal-select')) { - return command('.delete') + if (isTag(element)) { + return command(selectors.tag.close) } if (isHint(element)) { return command(selectors.hint.trigger) } + if (isDropDown(element)) { + return command(selectors.dropdown.trigger) + } + return originalFn(element, options) }) diff --git a/packages/testing/src/commands/overrides/focus.command.ts b/packages/testing/src/commands/overrides/focus.command.ts index 0185d9f8fb..143388a60e 100644 --- a/packages/testing/src/commands/overrides/focus.command.ts +++ b/packages/testing/src/commands/overrides/focus.command.ts @@ -3,6 +3,7 @@ import { isButton, isCheckbox, isDatepicker, + isDropDown, isInput, isInputDate, isNumberInput, @@ -62,5 +63,9 @@ Cypress.Commands.overwrite('focus', (originalFn: any, element: Cypress return command(selectors.select.input) } + if (isDropDown(element)) { + return command(selectors.dropdown.input) + } + return originalFn(element, options) }) diff --git a/packages/testing/src/commands/overrides/select.command.ts b/packages/testing/src/commands/overrides/select.command.ts index fd5d3772e3..adeff0ab29 100644 --- a/packages/testing/src/commands/overrides/select.command.ts +++ b/packages/testing/src/commands/overrides/select.command.ts @@ -1,4 +1,4 @@ -import { isSelect, isSteps, isTabs } from '../helpers' +import { isDropDown, isSelect, isSteps, isTabs, log, shouldLog } from '../helpers' import { selectors } from '../../selectors' import { byDataSelectors } from '../../selectors/selectors.util' @@ -29,6 +29,31 @@ Cypress.Commands.overwrite('select', (originalFn: any, element: any, values: any .wrap(element, { log: false }) } + if (isDropDown(element)) { + let valueArray: any[] = [] + if (typeof values === 'string') { + valueArray.push(values) + } else { + valueArray = [...values] + } + + log('select', valueArray, element, options) + + if (valueArray.length === 0) { + return cy.wrap(element, { log: false }).clear({ log: false }) + } + + return cy + .wrap(element, { log: false }) + .within({ log: false }, () => { + for (let index = 0; index < valueArray.length; index++) { + const label = valueArray[index] + cy.getByRole('option', { name: label, log: false }).click({ log: false }) + } + }) + .wrap(element, { log: false }) + } + if (isTabs(element)) { if (typeof values === 'string') { return cy diff --git a/packages/testing/src/commands/overrides/should.command.ts b/packages/testing/src/commands/overrides/should.command.ts index eeb07fc39e..aa66a05117 100644 --- a/packages/testing/src/commands/overrides/should.command.ts +++ b/packages/testing/src/commands/overrides/should.command.ts @@ -16,6 +16,7 @@ import { isSteps, hasTestId, isInputDate, + isDropDown, } from '../helpers' import { parseDataTestID, selectors } from '../../selectors/index' @@ -179,6 +180,51 @@ const shouldAndAndCommand = ( } } + if (hasClass(element, 'bal-dropdown__root__input')) { + const parseKey = () => { + return typeof key === 'string' + ? key + : (key as string[]) + .map(k => k.trim()) + .sort() + .join(',') + } + + switch (condition) { + case 'have.value': + return originalFn(element, 'have.attr', 'data-label', parseKey(), value) + + case 'not.have.value': + return originalFn(element, 'not.have.attr', 'data-label', parseKey(), value) + } + } + + if (isDropDown(element)) { + const nativeEl = element.find(selectors.dropdown.input, { log: false }) + const parseKey = () => { + return typeof key === 'string' + ? key + : (key as string[]) + .map(k => k.trim()) + .sort() + .join(',') + } + + switch (condition) { + case 'have.focus': + case 'not.have.focus': + case 'be.disabled': + case 'not.be.disabled': + return originalFn(nativeEl, condition, key, value, options) + + case 'have.value': + return originalFn(nativeEl, 'have.attr', 'data-label', parseKey(), value) + + case 'not.have.value': + return originalFn(nativeEl, 'not.have.attr', 'data-label', parseKey(), value) + } + } + if (isTabs(element)) { switch (condition) { case 'have.value': diff --git a/packages/testing/src/commands/overrides/type.command.ts b/packages/testing/src/commands/overrides/type.command.ts index 67b163455a..d0ace88d0d 100644 --- a/packages/testing/src/commands/overrides/type.command.ts +++ b/packages/testing/src/commands/overrides/type.command.ts @@ -1,4 +1,13 @@ -import { isInput, isInputDate, isNumberInput, isSlider, isTextarea, wrapCommand, wrapOptions } from '../helpers' +import { + isDropDown, + isInput, + isInputDate, + isNumberInput, + isSlider, + isTextarea, + wrapCommand, + wrapOptions, +} from '../helpers' import { selectors } from '../../selectors' Cypress.Commands.overwrite('type', (originalFn: any, element: any, content: any, options) => { @@ -8,6 +17,10 @@ Cypress.Commands.overwrite('type', (originalFn: any, element: any, content: any, return command(selectors.input.native) } + if (isDropDown(element)) { + return command(selectors.dropdown.input) + } + if (isInputDate(element)) { return command(selectors.dateInput.native) } diff --git a/packages/testing/src/selectors/index.ts b/packages/testing/src/selectors/index.ts index c4c873c2b3..1d282de49e 100644 --- a/packages/testing/src/selectors/index.ts +++ b/packages/testing/src/selectors/index.ts @@ -231,6 +231,24 @@ export const selectors = { */ chips: byTestId('bal-select-chip'), }, + dropdown: { + /** + * Native input element. + */ + input: '[data-native]', + /** + * Select option. + */ + options: 'bal-option', + /** + * Trigger to open and close the popup. + */ + trigger: byTestId('bal-dropdown-trigger'), + /** + * Multi select tag . + */ + chips: byTestId('bal-dropdown-chip'), + }, popover: { /** * Popover trigger. diff --git a/packages/vue/src/plugin.ts b/packages/vue/src/plugin.ts index 786f2f2c49..70a7a03a1f 100644 --- a/packages/vue/src/plugin.ts +++ b/packages/vue/src/plugin.ts @@ -11,7 +11,10 @@ interface BaloiseDesignSystemVueConfig { export const BaloiseDesignSystem: Plugin = { async install(app, config: BaloiseDesignSystemVueConfig = {}) { - initialize(config.defaults) + initialize({ + ...config.defaults, + httpFormSubmit: false, + }) if (config && config.defineCustomElementTag === true) { app.config.compilerOptions.isCustomElement = tag => tag.startsWith('bal-') diff --git a/resources/data/tags.json b/resources/data/tags.json index d2c4d86c8e..e6892c4981 100644 --- a/resources/data/tags.json +++ b/resources/data/tags.json @@ -11,6 +11,7 @@ "bal-data", "bal-date", "bal-divider", + "bal-dropdown", "bal-field", "bal-file-upload", "bal-footer", @@ -26,6 +27,7 @@ "bal-nav", "bal-notification", "bal-number-input", + "bal-option", "bal-pagination", "bal-popover", "bal-popup", diff --git a/test/angular/base/app/cypress/e2e/bal-dropdown-multiple.spec.cy.ts b/test/angular/base/app/cypress/e2e/bal-dropdown-multiple.spec.cy.ts new file mode 100644 index 0000000000..75f7101e98 --- /dev/null +++ b/test/angular/base/app/cypress/e2e/bal-dropdown-multiple.spec.cy.ts @@ -0,0 +1,39 @@ +describe('bal-dropdown-multiple', () => { + beforeEach(() => { + cy.visit('/').platform('desktop').waitForDesignSystem() + }) + it('should change value', () => { + cy.getByLabelText('Dropdown Multiple Label').click() + cy.getByTestId('dropdownMultiple').within(() => { + cy.getByRole('option', { name: 'Kiwi' }).click() + }) + cy.getByLabelText('Dropdown Multiple Label').click().blur() + + cy.getByLabelText('Dropdown Multiple Label') + .shouldBeInvalid() + .getDescribingElement() + .contains('This field is required') + + cy.getByLabelText('Dropdown Multiple Label').click() + cy.getByTestId('dropdownMultiple').within(() => { + cy.getByRole('option', { name: 'Kiwi' }).click() + cy.getByRole('option', { name: 'Mango' }).click() + }) + cy.getByLabelText('Dropdown Multiple Label').click().blur() + cy.getByLabelText('Dropdown Multiple Label').should('have.value', 'Mango,Kiwi') + + cy.getByLabelText('Dropdown Multiple Label') + .shouldBeValid() + .getDescribingElement() + .should('not.contain', 'This field is required') + + cy.getByRole('button', { name: 'Disable Dropdown Multiple' }).click() + cy.getByLabelText('Dropdown Multiple Label').should('be.disabled') + + cy.getByRole('button', { name: 'Enable Dropdown Multiple' }).click() + cy.getByLabelText('Dropdown Multiple Label').should('not.be.disabled') + + cy.getByTestId('result').contains(`"Kiwi"`) + cy.getByTestId('result').contains(`"Mango"`) + }) +}) diff --git a/test/angular/base/app/cypress/e2e/bal-dropdown.spec.cy.ts b/test/angular/base/app/cypress/e2e/bal-dropdown.spec.cy.ts new file mode 100644 index 0000000000..a02500f1c1 --- /dev/null +++ b/test/angular/base/app/cypress/e2e/bal-dropdown.spec.cy.ts @@ -0,0 +1,39 @@ +describe('bal-dropdown', () => { + beforeEach(() => { + cy.visit('/').platform('desktop').waitForDesignSystem() + }) + it('should change value', () => { + cy.getByLabelText('Dropdown Label') + .should('have.value', '') + .click() + .wait(32) + .click() + .blur() + .shouldBeInvalid() + .getDescribingElement() + .contains('This field is required') + + cy.getByLabelText('Dropdown Label').click() + cy.get('bal-dropdown').getByRole('option', { name: 'Kiwi' }).click() + cy.getByLabelText('Dropdown Label') + .should('have.value', 'Kiwi') + .shouldBeValid() + .getDescribingElement() + .should('not.contain', 'This field is required') + + cy.getByRole('button', { name: 'Update Dropdown' }).click() + cy.getByLabelText('Dropdown Label') + .should('have.value', 'Apple') + .shouldBeValid() + .getDescribingElement() + .should('not.contain', 'This field is required') + + cy.getByRole('button', { name: 'Disable Dropdown' }).click() + cy.getByLabelText('Dropdown Label').should('be.disabled') + + cy.getByRole('button', { name: 'Enable Dropdown' }).click() + cy.getByLabelText('Dropdown Label').should('not.be.disabled') + + cy.getByTestId('result').contains('"dropdown": "Apple"') + }) +}) diff --git a/test/angular/base/app/src/app/app.component.ts b/test/angular/base/app/src/app/app.component.ts index c2d263058b..5469ee7d35 100644 --- a/test/angular/base/app/src/app/app.component.ts +++ b/test/angular/base/app/src/app/app.component.ts @@ -9,6 +9,7 @@ import { DatePickerComponent } from './form-components/datepicker.component' import { TimeComponent } from './form-components/time.component' import { InputStepperComponent } from './form-components/input-stepper.component' import { SliderComponent } from './form-components/input-slider.component' +import { DropdownComponent } from './form-components/dropdown.component' import { SelectComponent } from './form-components/select.component' import { CheckboxComponent } from './form-components/checkbox.component' import { CheckboxGroupComponent } from './form-components/checkbox-group.component' @@ -39,6 +40,7 @@ export interface UpdateControl { TimeComponent, InputStepperComponent, SliderComponent, + DropdownComponent, SelectComponent, CheckboxComponent, CheckboxGroupComponent, @@ -61,6 +63,8 @@ export interface UpdateControl { + + @@ -97,6 +101,8 @@ export class AppComponent { time: new FormControl(null, [Validators.required]), inputStepper: new FormControl(0, [Validators.min(2)]), slider: new FormControl(30, [Validators.min(10)]), + dropdown: new FormControl(null, [Validators.required]), + dropdownMultiple: new FormControl(['Kiwi'], [Validators.required]), select: new FormControl('Kiwi', [Validators.required]), selectMultiple: new FormControl(['Kiwi'], [Validators.required]), typeahead: new FormControl('Kiwi', [Validators.required]), diff --git a/test/angular/base/app/src/app/form-components/dropdown.component.ts b/test/angular/base/app/src/app/form-components/dropdown.component.ts new file mode 100644 index 0000000000..7a7834db8f --- /dev/null +++ b/test/angular/base/app/src/app/form-components/dropdown.component.ts @@ -0,0 +1,63 @@ +import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core' +import { CommonModule } from '@angular/common' +import { FormGroup, ReactiveFormsModule } from '@angular/forms' +import { balImports } from '../../design-system' +import { UpdateControl } from '../app.component' + +@Component({ + selector: 'app-dropdown', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, ...balImports], + template: ` + + {{ label }} + + + {{ label }} Label + + + {{ option }} + + + + This field is required + + + + + Update {{ label }} + + Enable {{ label }} + Disable {{ label }} + + + + `, + styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class DropdownComponent { + @Input() form!: FormGroup + @Input() multiple = false + @Input() typeahead = false + + get label() { + return this.multiple ? 'Dropdown Multiple' : 'Dropdown' + } + + get control() { + return this.multiple ? 'dropdownMultiple' : 'dropdown' + } + + @Output() updateControl = new EventEmitter() + + options = ['Apple', 'Banana', 'Orange', 'Mango', 'Strawberry', 'Pineapple', 'Grapes', 'Watermelon', 'Kiwi', 'Peach'] +}