diff --git a/components/accordion/accordion.component.yml b/components/accordion/accordion.component.yml new file mode 100644 index 000000000..45665ea4f --- /dev/null +++ b/components/accordion/accordion.component.yml @@ -0,0 +1,47 @@ +name: Accordion +description: 'List of items that can be clicked to hide or reveal additional content.' +props: + type: object + properties: + accordion_id: + title: 'Accordion ID' + description: 'Unique ID for the accordion. Defaults to a random number.' + type: string + bare_layout: + title: 'Bare layout' + description: 'Enable to remove the default background-color, some borders, and some rounded corners printing the accordion edge-to-edge with their parent container. Defaults to false.' + type: boolean + open_item: + title: 'Accordion item opened by default' + description: 'Index value of the item to open (starting from 1), all closed by default (0).' + type: number + title: + title: Title + type: [string, array, object] + description: 'Title of the accordion.' + title_tag: + title: 'Title tag' + description: 'The tag to use for the title. Defaults to h2.' + type: string + expand_button_label: + title: 'Expand button' + description: 'Label of the expand button.' + type: [string, array, object] + collapse_button_label: + title: 'Collapse button' + description: 'Label of the collapse button.' + type: [string, array, object] + items: + title: 'Accordion items' + description: 'List of accordion items.' + type: array + items: + type: object + properties: + title: + type: [string, array, object] + content: + type: [string, array, object] + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/accordion/accordion.story.twig b/components/accordion/accordion.story.twig new file mode 100644 index 000000000..bf9109b07 --- /dev/null +++ b/components/accordion/accordion.story.twig @@ -0,0 +1,24 @@ +{% set items = [ + { + title: 'Jobs, Growth, Investment and Competitiveness with an additional quite long string', + content: 'Lorem fistrum eiusmod cillum elit papaar officia aliquip dolore velit. Pellentesque dolor sapien, mollis fringilla ultricies sed, lacinia vel nibh. Nulla auctor arcu risus, nec facilisis odio aliquet sed.', + }, + { + title: 'Energy Union', + content: 'Advice on living, working or travelling in the EU, on visas and immigration for non-EU citizens, European culture. Plus some extra text to make it a little bit longer.', + }, +] %} + +{% include 'oe_bootstrap_theme:accordion' with { + title: 'This is the title of this accordion.', + items: items, + open_item: 1, +} only %} + +{% include 'oe_bootstrap_theme:accordion' with { + title: 'This is the title of this accordion with expand/collapse buttons.', + expand_button_label: 'Expand', + collapse_button_label: 'Collapse', + items: items, + open_item: 1, +} only %} diff --git a/components/accordion/accordion.twig b/components/accordion/accordion.twig new file mode 100644 index 000000000..05e09e92f --- /dev/null +++ b/components/accordion/accordion.twig @@ -0,0 +1,22 @@ +{# +/** + * @file + * Accordion pattern. + */ +#} +{% include '@oe-bcl/accordion' with { + title: title, + title_tag: title_tag, + id: accordion_id|default(html_unique_id('bcl-accordion')), + flush: bare_layout, + items: items, + open_item_id: open_item, + expand_button: expand_button_label ? { + label: expand_button_label, + }, + collapse_button: collapse_button_label ? { + label: collapse_button_label, + variant: 'secondary', + }, + attributes: attributes, +} only %} diff --git a/components/alert/alert.component.yml b/components/alert/alert.component.yml new file mode 100644 index 000000000..1e2c94b70 --- /dev/null +++ b/components/alert/alert.component.yml @@ -0,0 +1,56 @@ +name: 'Alert' +description: 'Provide contextual feedback messages for typical user actions with the handful of available and flexible alert messages.' +props: + type: object + properties: + variant: + title: Variant + type: string + description: 'The alert variant. Defaults to primary.' + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + - link + 'meta:enum': + primary: 'Primary (default)' + secondary: Secondary + success: Success + danger: Danger + warning: Warning + info: Info + light: Light + dark: Dark + link: Link + dismissible: + title: Dismissible + type: boolean + description: 'Adds close button to dismiss the alert inline. Defaults to true.' + animated_dismiss: + title: 'Animated dismiss' + type: boolean + description: 'Adds fade animation to the dismissible alert. Defaults to true.' + heading: + title: Heading + type: [string, array, object] + description: 'The alert heading.' + message: + title: Message + type: [string, array, object] + description: 'The alert message.' + icon: + title: 'Icon name' + type: string + description: 'Icon name to display in the alert.' + icon_path: + title: 'Icons path' + type: string + description: 'Path to icons.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/alert/alert.story.twig b/components/alert/alert.story.twig new file mode 100644 index 000000000..0a330329a --- /dev/null +++ b/components/alert/alert.story.twig @@ -0,0 +1,22 @@ +{% set variants = [ + 'primary', + 'secondary', + 'success', + 'danger', + 'warning', + 'info', + 'light', + 'dark', + 'link', +] %} + +{% for variant in variants %} + {% include 'oe_bootstrap_theme:alert' with { + variant: variant, + dismissible: true, + animated_dismiss: true, + heading: variant|capitalize ~ ' alert', + message: 'A simple alert. Check it out!', + icon: 'lock', + } only %} +{% endfor %} diff --git a/components/alert/alert.twig b/components/alert/alert.twig new file mode 100644 index 000000000..834310e59 --- /dev/null +++ b/components/alert/alert.twig @@ -0,0 +1,16 @@ +{# +/** + * @file + * Alert component. + */ +#} +{% include '@oe-bcl/alert' with { + heading: heading, + message: message, + variant: variant, + dismissible: dismissible, + animated_dismiss: animated_dismiss, + icon_path: icon_path|default(bcl_icon_path|default(bcl_icon_path())), + icon_name: icon, + attributes: attributes, +} only %} diff --git a/components/badge/badge.component.yml b/components/badge/badge.component.yml new file mode 100644 index 000000000..59f97fffc --- /dev/null +++ b/components/badge/badge.component.yml @@ -0,0 +1,62 @@ +name: 'Badge' +description: 'A small count and labeling component. Badges scale to match the size of the immediate parent element by using relative font sizing and em units.' +props: + type: object + properties: + label: + title: Label + type: [string, array, object] + description: 'The badge label.' + url: + title: 'Badge URL' + type: string + description: 'Converts the badge in a link.' + title: + title: 'Link title' + type: string + description: 'Title attribute if the badge is a link.' + assistive_text: + title: 'Assistive text' + type: string + description: 'Text to visually hide before the badge label.' + background: + title: 'Badge background' + type: string + description: 'Set the background color of the badge. Defaults to primary.' + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + 'meta:enum': + primary: 'Primary (default)' + secondary: Secondary + success: Success + danger: Danger + warning: Warning + info: Info + light: Light + dark: Dark + rounded_pill: + title: 'Pill badge' + type: boolean + description: 'Makes the badge a rounded pill. Defaults to false.' + dismissible: + title: 'Dismissible badge' + type: boolean + description: 'Adds a close icon at the end of the badge. Defaults to false.' + outline: + title: Outline + type: boolean + description: 'Uses a border instead of a background. Defaults to false.' + icon_path: + title: 'Icons path' + type: string + description: 'Path to icons.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/badge/badge.story.twig b/components/badge/badge.story.twig new file mode 100644 index 000000000..ad4e65aaf --- /dev/null +++ b/components/badge/badge.story.twig @@ -0,0 +1,47 @@ +{% set badges = [ + { label: 'Primary', background: 'primary' }, + { label: 'Secondary', background: 'secondary' }, + { label: 'Success', background: 'success' }, + { label: 'Danger', background: 'danger' }, + { label: 'Warning', background: 'warning' }, + { label: 'Info', background: 'info' }, + { label: 'Light', background: 'light' }, + { label: 'Dark', background: 'dark' }, +] %} + +
+
+ {% for badge in badges %} + {% include 'oe_bootstrap_theme:badge' with badge only %} + {% endfor %} +
+ +
+ {% for badge in badges %} + {% include 'oe_bootstrap_theme:badge' with badge|merge({ outline: true }) only %} + {% endfor %} +
+ +
+ {% include 'oe_bootstrap_theme:badge' with { + label: 'Rounded pill', + background: 'success', + rounded_pill: true, + } only %} + + {% include 'oe_bootstrap_theme:badge' with { + label: 'Linked badge', + url: 'https://example.com', + title: 'The badge title', + background: 'info', + } only %} + + {% include 'oe_bootstrap_theme:badge' with { + label: 'Dismissible', + background: 'danger', + rounded_pill: true, + dismissible: true, + assistive_text: 'Remove badge', + } only %} +
+
diff --git a/components/badge/badge.twig b/components/badge/badge.twig new file mode 100644 index 000000000..405b08c1a --- /dev/null +++ b/components/badge/badge.twig @@ -0,0 +1,19 @@ +{# +/** + * @file + * Badge component. + */ +#} + +{% include '@oe-bcl/badge' with { + label: label, + title: title, + url: url, + assistive_text: assistive_text, + rounded_pill: rounded_pill, + background: background, + icon_path: icon_path|default(bcl_icon_path|default(bcl_icon_path())), + dismissible: dismissible, + outline: outline, + attributes: attributes, +} only %} diff --git a/components/banner/banner.component.yml b/components/banner/banner.component.yml new file mode 100644 index 000000000..f657399b2 --- /dev/null +++ b/components/banner/banner.component.yml @@ -0,0 +1,85 @@ +name: 'Banner' +description: 'Banner or heroes with text and/or background image and/or with call to action.' +props: + type: object + properties: + variant: + title: Variant + type: string + description: 'The banner variant. Defaults to default.' + default: default + enum: + - default + - primary + - image + - image_shade + 'meta:enum': + default: 'Default text banner' + primary: 'Primary text banner' + image: 'Image banner' + image_shade: 'Image shade banner' + hero: + title: Hero + type: boolean + description: 'Displays as hero banner. Defaults to false.' + shade: + title: Shade + type: boolean + description: 'Enables the shadow effect to the image. Defaults to true for the image shade variant and false otherwise.' + full_width: + title: 'Full width' + type: boolean + description: 'Allows a full-width banner within a bootstrap container. Defaults to false.' + centered: + title: Centered + type: boolean + description: 'Whether the content of the banner is centered or not. Defaults to false.' + title: + title: Title + type: [string, array, object] + description: 'Title of the banner.' + title_tag: + title: 'Title tag' + type: string + description: 'The tag to use for the title. Defaults to div.' + description: + title: Description + type: [string, array, object] + description: 'Sub-heading of the banner.' + call_to_action: + title: 'Call to action' + type: object + description: 'Link to print a CTA button.' + properties: + label: + type: [string, array, object] + path: + type: string + link_style: + type: string + icon_position: + type: string + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + image: + title: Image + type: string + description: 'URL of the image.' + copyright: + title: Copyright + type: [string, array, object] + description: 'Optional copyright text displayed below the banner.' + content_classes: + title: 'Content classes' + type: string + description: 'Classes for the content wrapper.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/banner/banner.story.twig b/components/banner/banner.story.twig new file mode 100644 index 000000000..15023ae60 --- /dev/null +++ b/components/banner/banner.story.twig @@ -0,0 +1,47 @@ +{% set image = 'https://picsum.photos/1000/500/' %} +{% set call_to_action = { + label: 'Subscribe', + path: 'https://example.com', + link_style: 'warning', + icon_position: 'after', + icon: { + name: 'chevron-right', + path: bcl_icon_path(), + }, +} %} + +{% include 'oe_bootstrap_theme:banner' with { + variant: 'default', + full_width: true, + centered: true, + title: 'EU Budget for the future', + title_tag: 'h4', + description: 'Innovation, economy, environment and geopolitics', + call_to_action: call_to_action, +} only %} + +{% include 'oe_bootstrap_theme:banner' with { + variant: 'primary', + hero: true, + full_width: true, + title: 'EU Budget for the future', + description: 'Innovation, economy, environment and geopolitics', +} only %} + +{% include 'oe_bootstrap_theme:banner' with { + variant: 'image', + title: 'EU Budget for the future', + description: 'Innovation, economy, environment and geopolitics', + copyright: '© 2024 Example Organisation', + image: image, +} only %} + +{% include 'oe_bootstrap_theme:banner' with { + variant: 'image_shade', + full_width: true, + centered: true, + title: 'EU Budget for the future', + description: 'Innovation, economy, environment and geopolitics', + call_to_action: call_to_action, + image: image, +} only %} diff --git a/components/banner/banner.twig b/components/banner/banner.twig new file mode 100644 index 000000000..44c85ae63 --- /dev/null +++ b/components/banner/banner.twig @@ -0,0 +1,47 @@ +{# +/** + * @file + * Banner component. + */ +#} +{% set _variant = variant|default('default') %} +{% set _shade = shade ?? _variant == 'image_shade' %} +{% set _attributes = attributes|default(create_attribute()) %} + +{% if _variant == 'primary' %} + {% set _attributes = _attributes.addClass(['bg-primary', 'text-white']) %} +{% elseif _variant == 'image_shade' %} + {% set _attributes = _attributes.addClass(['bg-lighter']) %} +{% else %} + {% set _attributes = _attributes.addClass(['bg-lighter', 'text-dark']) %} +{% endif %} + +{% if centered %} + {% set _attributes = _attributes.addClass(['text-center']) %} +{% endif %} + +{% set _call_to_action = call_to_action|default({}) %} +{% if _call_to_action and _call_to_action.link_style is defined %} + {% set _call_to_action = _call_to_action|merge({variant: _call_to_action.link_style}) %} +{% endif %} +{% if _call_to_action.icon is defined and _call_to_action.icon is not empty and _call_to_action.icon.path is not defined %} + {% set _call_to_action = _call_to_action|merge({ + icon: _call_to_action.icon|merge({path: bcl_icon_path()}) + }) %} +{% endif %} + +{% include '@oe-bcl/banner' with { + shade: _shade, + hero: hero, + full_width: full_width, + centered: centered, + title: title, + title_tag: title_tag, + description: description, + link: _call_to_action, + copyright: _variant in ['image', 'image_shade'] ? copyright : '', + copyright_label: _variant in ['image', 'image_shade'] ? 'Image credit:'|t : '', + image: _variant in ['image', 'image_shade'] ? image : '', + content_classes: content_classes, + attributes: _attributes, +} only %} diff --git a/components/blockquote/blockquote.component.yml b/components/blockquote/blockquote.component.yml new file mode 100644 index 000000000..0364d2c5d --- /dev/null +++ b/components/blockquote/blockquote.component.yml @@ -0,0 +1,40 @@ +name: 'Blockquote' +description: 'For quoting blocks of content from another source within your document.' +props: + type: object + properties: + title: + title: Title + type: [string, array, object] + description: 'Title of the blockquote.' + title_tag: + title: 'Title tag' + type: string + description: 'The tag to use for the title. Defaults to h2.' + quote: + title: Quote + type: [string, array, object] + description: 'The quote text.' + attribution: + title: Attribution + type: [string, array, object] + description: 'The name of the quoted person or entity.' + cite: + title: Citation + type: [string, array, object] + description: 'The creative work in which the referenced quote was published.' + alignment: + title: 'Text alignment' + type: string + description: 'Aligns the quote in the page.' + enum: + - left + - center + - end + 'meta:enum': + left: 'left (default)' + center: center + end: end + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/blockquote/blockquote.story.twig b/components/blockquote/blockquote.story.twig new file mode 100644 index 000000000..636f5da8a --- /dev/null +++ b/components/blockquote/blockquote.story.twig @@ -0,0 +1,33 @@ +{% set _quote = 'A well-known quote, contained in a blockquote element.' %} +{% set _attribution = 'Someone famous in' %} +{% set _cite = 'Source title' %} + +

Left alignment

+{% include 'oe_bootstrap_theme:blockquote' with { + title: 'The blockquote title', + title_tag: 'h4', + alignment: 'left', + quote: _quote, + attribution: _attribution, + cite: _cite, +} only %} + +

Center alignment

+{% include 'oe_bootstrap_theme:blockquote' with { + title: 'The blockquote title', + title_tag: 'h4', + alignment: 'center', + quote: _quote, + attribution: _attribution, + cite: _cite, +} only %} + +

Right alignment

+{% include 'oe_bootstrap_theme:blockquote' with { + title: 'The blockquote title', + title_tag: 'h4', + alignment: 'end', + quote: _quote, + attribution: _attribution, + cite: _cite, +} only %} diff --git a/components/blockquote/blockquote.twig b/components/blockquote/blockquote.twig new file mode 100644 index 000000000..11f7a5d89 --- /dev/null +++ b/components/blockquote/blockquote.twig @@ -0,0 +1,17 @@ +{# +/** + * @file + * Blockquote component. + */ +#} +{% set attribution = attribution and cite ? attribution ~ ' ' : attribution %} + +{% include '@oe-bcl/blockquote' with { + title: title, + title_tag: title_tag, + quote: quote, + attribution: attribution, + cite: cite, + alignment: alignment, + attributes: attributes, +} only %} diff --git a/components/breadcrumb/breadcrumb.component.yml b/components/breadcrumb/breadcrumb.component.yml new file mode 100644 index 000000000..ec1df86d3 --- /dev/null +++ b/components/breadcrumb/breadcrumb.component.yml @@ -0,0 +1,34 @@ +name: 'Breadcrumb' +description: 'Indicate the current page location within a navigational hierarchy that automatically adds separators via CSS.' +props: + type: object + properties: + items: + title: Items + type: array + description: 'List with the breadcrumb items.' + items: + type: object + properties: + label: + title: Label + type: [string, array, object] + description: 'Link label text or render array.' + path: + title: Path + type: [string, object] + description: 'Link URL string or URL object.' + id: + title: ID + type: string + description: 'Optional link ID.' + attributes: + title: Attributes + type: object + icon_path: + title: 'Icons path' + type: string + description: 'Path to icons.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/breadcrumb/breadcrumb.story.twig b/components/breadcrumb/breadcrumb.story.twig new file mode 100644 index 000000000..36ccb1fad --- /dev/null +++ b/components/breadcrumb/breadcrumb.story.twig @@ -0,0 +1,35 @@ +

Last item as a link

+{% include 'oe_bootstrap_theme:breadcrumb' with { + items: [ + { + label: 'Home', + path: '/index', + }, + { + label: 'About the European Commission', + path: '/example', + }, + { + label: 'Link title', + path: 'https://example.com', + }, + ], +} only %} + +

Current page without link

+{% include 'oe_bootstrap_theme:breadcrumb' with { + attributes: create_attribute().addClass('mt-3'), + items: [ + { + label: 'Home', + path: '/index', + }, + { + label: 'About the European Commission', + path: '/example', + }, + { + label: 'Current page', + }, + ], +} only %} diff --git a/components/breadcrumb/breadcrumb.twig b/components/breadcrumb/breadcrumb.twig new file mode 100644 index 000000000..3a6539f9c --- /dev/null +++ b/components/breadcrumb/breadcrumb.twig @@ -0,0 +1,18 @@ +{# +/** + * @file + * Breadcrumb component. + */ +#} +{% set _items = [] %} +{% for item in items|default([]) %} + {% set _items = _items|merge([item|merge({ + attributes: create_attribute(item.attributes|default({})), + })]) %} +{% endfor %} + +{% include '@oe-bcl/breadcrumb' with { + links: _items, + icon_path: icon_path|default(bcl_icon_path()), + attributes: attributes, +} only %} diff --git a/components/button/button.component.yml b/components/button/button.component.yml new file mode 100644 index 000000000..3b041db9e --- /dev/null +++ b/components/button/button.component.yml @@ -0,0 +1,142 @@ +name: 'Button' +description: 'For actions in forms, dialogs, and more with support for multiple sizes, states, and icons.' +props: + type: object + properties: + label: + title: Label + type: [string, array, object] + description: 'The button label.' + variant: + title: Variant + type: string + description: 'The button variant. Defaults to primary.' + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + - link + 'meta:enum': + primary: 'Primary (default)' + secondary: Secondary + success: Success + danger: Danger + warning: Warning + info: Info + light: Light + dark: Dark + link: Link + size: + title: Size + type: string + description: 'Size of the button. Defaults to md.' + enum: + - sm + - md + - lg + 'meta:enum': + sm: Small + md: 'Medium (default)' + lg: Large + type: + title: Type + type: string + description: 'Sets the button behavior type. Defaults to button.' + enum: + - button + - submit + - reset + 'meta:enum': + button: 'Button (default)' + submit: Submit + reset: Reset + outline: + title: Outline + type: boolean + description: 'Lighter button style removing background color. Defaults to false.' + disabled: + title: Disabled + type: boolean + description: 'If true, the button will not be enabled when clicking. Defaults to false.' + text_nowrap: + title: 'No text wrap' + type: boolean + description: 'Prevents the button label from wrapping.' + assistive_text: + title: 'Assistive text' + type: string + description: 'Visually hidden text printed before the button label.' + id: + title: ID + type: string + description: 'Button ID attribute.' + icon: + title: Icon + description: 'Icon name as a string or an icon object.' + type: [string, object] + properties: + name: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + title: 'Icon position' + type: string + description: 'If button has an icon, it can be placed before or after text. Defaults to after.' + enum: + - before + - after + 'meta:enum': + before: Before + after: 'After (default)' + icon_spacing: + title: 'Icon spacing' + type: string + description: 'Spacing between icon and label.' + enum: + - sm + - md + - lg + 'meta:enum': + sm: Small + md: Medium + lg: Large + spinner: + title: Spinner + type: object + description: 'Spinner object.' + properties: + type: + type: string + variant: + type: string + size: + type: string + assistive_text: + type: string + attributes: + type: object + show_spinner: + title: 'Show spinner' + type: boolean + description: 'Displays the spinner before the button label.' + clean_class: + title: 'Clean class' + type: boolean + description: 'If true, BCL button classes are not added.' + remove_icon_spacers: + title: 'Remove icon spacers' + type: boolean + description: 'Removes the default spacing between icon and label.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/button/button.story.twig b/components/button/button.story.twig new file mode 100644 index 000000000..6a7387b67 --- /dev/null +++ b/components/button/button.story.twig @@ -0,0 +1,104 @@ +{% set variants = [ + 'primary', + 'secondary', + 'success', + 'danger', + 'warning', + 'info', + 'light', + 'dark', + 'link', +] %} + +
+
+ {% for variant in variants %} + {% include 'oe_bootstrap_theme:button' with { + label: variant|capitalize, + variant: variant, + } only %} + {% endfor %} +
+ +
+ {% include 'oe_bootstrap_theme:button' with { + label: 'Outline', + variant: 'primary', + outline: true, + } only %} + + {% include 'oe_bootstrap_theme:button' with { + label: 'Small', + variant: 'secondary', + size: 'sm', + } only %} + + {% include 'oe_bootstrap_theme:button' with { + label: 'Large', + variant: 'warning', + size: 'lg', + } only %} + + {% include 'oe_bootstrap_theme:button' with { + label: 'Disabled', + variant: 'dark', + disabled: true, + } only %} +
+ +
+ {% include 'oe_bootstrap_theme:button' with { + label: 'Icon before', + variant: 'primary', + icon: 'arrow-right', + icon_position: 'before', + } only %} + + {% include 'oe_bootstrap_theme:button' with { + label: 'Icon after', + variant: 'primary', + icon: 'arrow-right', + icon_position: 'after', + } only %} + + {% include 'oe_bootstrap_theme:button' with { + label: 'Large icon spacing', + variant: 'primary', + icon: 'arrow-right', + icon_spacing: 'lg', + } only %} + + {% include 'oe_bootstrap_theme:button' with { + label: 'Rotated icon', + variant: 'primary', + icon: { + name: 'arrow-right', + transformation: 'rotate-90', + }, + } only %} + + {% include 'oe_bootstrap_theme:button' with { + label: 'No icon spacer', + variant: 'primary', + icon: 'arrow-right', + remove_icon_spacers: true, + } only %} + + {% include 'oe_bootstrap_theme:button' with { + label: 'Loading', + variant: 'primary', + show_spinner: true, + spinner: { + size: 'sm', + assistive_text: 'Loading', + }, + } only %} +
+ +
+ {% include 'oe_bootstrap_theme:button' with { + label: 'Clean class', + clean_class: true, + } only %} +
+
diff --git a/components/button/button.twig b/components/button/button.twig new file mode 100644 index 000000000..d794d1cb3 --- /dev/null +++ b/components/button/button.twig @@ -0,0 +1,48 @@ +{# +/** + * @file + * Button component. + */ +#} +{% set _icon = {} %} +{% if icon is defined and icon %} + {% set _icon = icon is iterable ? icon : { name: icon } %} + {% if _icon.path is not defined or _icon.path is empty %} + {% set _icon = _icon|merge({ path: bcl_icon_path() }) %} + {% endif %} + {% set _icon = _icon|merge({ + attributes: create_attribute(_icon.attributes|default({})), + }) %} +{% endif %} + +{% set _spinner = spinner|default({}) %} +{% if _spinner is not empty %} + {% set _spinner = _spinner|merge({ + attributes: create_attribute(_spinner.attributes|default({})), + }) %} +{% endif %} + +{% set _attributes = attributes|default(create_attribute()) %} +{% if show_spinner is defined and show_spinner and label is defined and label %} + {% set _attributes = _attributes.addClass(['d-inline-flex', 'align-items-center', 'gap-2']) %} +{% endif %} + +{% include '@oe-bcl/button' with { + label: label, + variant: variant|default('primary'), + size: size|default('md'), + type: type|default('button'), + outline: outline, + disabled: disabled ?? _attributes.disabled ?? false, + text_nowrap: text_nowrap, + assistive_text: assistive_text, + id: id, + icon: _icon, + icon_position: icon_position|default('after'), + icon_spacing: icon_spacing, + spinner: _spinner, + show_spinner: show_spinner, + clean_class: clean_class, + remove_icon_spacers: remove_icon_spacers, + attributes: _attributes, +} only %} diff --git a/components/card/card.component.yml b/components/card/card.component.yml new file mode 100644 index 000000000..b6d798d34 --- /dev/null +++ b/components/card/card.component.yml @@ -0,0 +1,166 @@ +name: Card +description: 'A card is a flexible and extensible content container. It includes options for headers and footers, a wide variety of content, contextual background colors, and powerful display options.' +props: + type: object + properties: + variant: + title: Variant + type: string + description: 'The card variant. Defaults to default.' + enum: + - default + - search + 'meta:enum': + default: 'Default' + search: Search + orientation: + title: Orientation + type: string + description: 'Can be set to horizontal or vertical. Defaults to vertical.' + enum: + - vertical + - horizontal + 'meta:enum': + vertical: 'Vertical (default)' + horizontal: Horizontal + title: + title: Title + type: [string, array, object] + description: 'Title of the card.' + title_tag: + title: 'Title tag' + type: string + description: 'Title tag of the card. Defaults to div.' + title_link: + title: 'Title link' + type: object + description: 'Optional link object for the title.' + properties: + label: + type: [string, array, object] + path: + type: string + variant: + type: string + id: + type: string + disabled: + type: boolean + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + icon_position: + type: string + icon_display: + type: string + remove_icon_spacers: + type: boolean + attributes: + type: object + subtitle: + title: Subtitle + type: [string, array, object] + description: 'Card subtitle.' + text: + title: Text + type: [string, array, object] + description: 'Card text.' + image: + title: Image + type: object + description: 'Image for the card. Optional.' + properties: + src: + type: string + alt: + type: string + position: + type: string + classes: + type: string + rounded: + type: string + image_copyright: + title: 'Image copyright' + type: [string, array, object] + description: 'Optional copyright text displayed as an overlay.' + date: + title: 'Date block' + type: object + description: 'If set, presents a date block that uses the same parameters as the BCL date-block template. Optional.' + properties: + day: + type: [string, integer] + month: + type: string + date_time: + type: string + year: + type: [string, integer] + end_day: + type: [string, integer] + end_month: + type: string + end_year: + type: [string, integer] + icon_path: + type: string + attributes: + type: object + meta: + title: 'Meta items' + type: array + description: 'Date or other meta information.' + items: + type: [string, array, object] + content: + title: Content + type: [string, array, object] + description: 'Card content.' + header: + title: 'Card header' + type: [string, array, object] + description: 'Card header content.' + footer: + title: 'Card footer' + type: [string, array, object] + description: 'Card footer content.' + badges: + title: Badges + type: array + description: 'List of badges for the card.' + items: + type: [string, object] + properties: + label: + type: [string, array, object] + background: + type: string + url: + type: string + title: + type: string + dismissible: + type: boolean + assistive_text: + type: string + rounded_pill: + type: boolean + outline: + type: boolean + icon_path: + type: string + attributes: + type: object + title_attributes: + title: 'Title attributes' + type: Drupal\Core\Template\Attribute + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/card/card.story.twig b/components/card/card.story.twig new file mode 100644 index 000000000..12b0679c6 --- /dev/null +++ b/components/card/card.story.twig @@ -0,0 +1,119 @@ +
+ {% include 'oe_bootstrap_theme:card' with { + variant: 'default', + orientation: 'vertical', + title: 'Vertical card', + title_tag: 'h2', + subtitle: 'Subtitle card', + text: 'This card includes a longer description to show how body text wraps across multiple lines in the component preview.', + image: { + src: 'https://picsum.photos/1000/500/', + alt: 'Alternative text for card image', + }, + image_copyright: '© 2026 Example Organisation', + content: { + '#type': 'html_tag', + '#tag': 'button', + '#value': 'Custom action', + '#attributes': { + class: ['btn', 'btn-primary'], + }, + }, + header: 'Header of card', + footer: 'Footer of card', + } only %} + + {% include 'oe_bootstrap_theme:card' with { + variant: 'default', + orientation: 'horizontal', + title: 'Horizontal card', + title_tag: 'h2', + subtitle: 'Subtitle card', + text: 'This card includes a longer description to show how body text wraps across multiple lines in the component preview.', + image: { + src: 'https://picsum.photos/1000/500/', + alt: 'Alternative text for card image', + }, + image_copyright: '© 2026 Example Organisation', + meta: [ + 'DIGIT', + 'Developer', + ], + badges: [ + { label: 'A badge' }, + { label: 'A non-rounded badge', rounded_pill: false }, + { label: 'A solid badge', outline: false }, + ], + } only %} + + {% include 'oe_bootstrap_theme:card' with { + variant: 'search', + title: 'Search result card', + title_tag: 'h3', + text: 'This card includes a longer description to show how body text wraps across multiple lines in the component preview.', + image: { + src: 'https://picsum.photos/1000/500/', + alt: 'Alternative text for card image', + }, + badges: [ + { label: 'Category 1' }, + { label: 'Category 2' }, + ], + } only %} + + {% include 'oe_bootstrap_theme:card' with { + variant: 'search', + title: 'Search result with date', + text: 'This card includes a longer description to show how body text wraps across multiple lines in the component preview.', + date: { + year: '2021', + month: 'Jan', + day: '23', + date_time: '2021-01-23', + }, + badges: [ + { label: 'Category 1' }, + { label: 'Category 2' }, + ], + } only %} + + {% include 'oe_bootstrap_theme:card' with { + variant: 'default', + orientation: 'horizontal', + title: 'Card with date range', + text: 'This card includes a longer description to show how body text wraps across multiple lines in the component preview.', + date: { + year: '2021', + month: 'Jan', + day: '23', + end_year: '2022', + end_month: 'Feb', + end_day: '11', + }, + } only %} + + {% include 'oe_bootstrap_theme:card' with { + variant: 'default', + title: 'Card with a linked title', + title_link: { + path: 'https://example.com', + variant: 'warning', + icon: { + name: 'arrow-right', + }, + }, + text: 'This card includes a longer description to show how body text wraps across multiple lines in the component preview.', + } only %} + + {% include 'oe_bootstrap_theme:card' with { + variant: 'default', + orientation: 'horizontal', + title: 'Card with a rounded image', + text: 'This card includes a longer description to show how body text wraps across multiple lines in the component preview.', + image: { + src: 'https://picsum.photos/500/500/', + alt: 'Alternative text for card image', + rounded: 'circle', + }, + } only %} +
diff --git a/components/card/card.twig b/components/card/card.twig new file mode 100644 index 000000000..096749e2e --- /dev/null +++ b/components/card/card.twig @@ -0,0 +1,171 @@ +{# +/** + * @file + * Card component. + */ +#} +{%- set _variant = variant|default('default') -%} +{%- set _orientation = orientation|default('vertical') -%} +{%- set _attributes = attributes|default(create_attribute()) -%} + +{%- set _title_link = title_link|default({}) -%} +{%- if _title_link.icon is defined and _title_link.icon is not empty and _title_link.icon.path is not defined -%} + {%- set _title_link = _title_link|merge({ + icon: _title_link.icon|merge({path: bcl_icon_path()}) + }) -%} +{%- endif -%} +{%- if _title_link is not empty -%} + {%- set _title_link = _title_link|merge({ + attributes: create_attribute(_title_link.attributes|default({})), + }) -%} +{%- endif -%} + +{%- set _content -%} + {%- if meta is not empty -%} + {# Use negative margin to compensate for me-3 on each item. #} +
+ {%- for _meta_item in meta -%} + {%- if _meta_item is not empty -%} + {{- _meta_item -}} + {%- endif -%} + {%- endfor -%} +
+ {%- endif -%} + {%- set _rendered_content -%} + {{- content -}} + {%- endset -%} + {%- if _rendered_content|trim is not empty -%} +
{{- _rendered_content -}}
+ {%- endif -%} +{%- endset -%} + +{%- set _badges = [] -%} +{%- if badges is not empty -%} + {%- for item in badges -%} + {%- set _badge = item is iterable ? item : { label: item } -%} + {%- set _badge = _badge|merge({ + attributes: create_attribute(_badge.attributes|default({})), + }) -%} + {%- set _badges = _badges|merge([{ + outline: true, + rounded_pill: true, + }|merge(_badge)]) -%} + {%- endfor -%} +{%- endif -%} + +{%- set _image = {} -%} +{%- if image is not empty -%} + {%- set _image = image|merge({ + path: image.src|default(''), + alt: image.alt|default(''), + }) -%} +{%- endif -%} + +{%- set _date = date|default({}) -%} +{%- if _date is not empty -%} + {%- if _date.icon_path is not defined or _date.icon_path is empty -%} + {%- set _date = _date|merge({ + icon_path: bcl_icon_path(), + }) -%} +{%- endif -%} + {%- set _date = _date|merge({ + attributes: create_attribute(_date.attributes|default({})), + }) -%} +{%- endif -%} +{%- set _horizontal_grid = {} -%} +{%- if _orientation == 'horizontal' -%} + {%- set _horizontal_grid = { + left_col_classes: 'col-4', + right_col_classes: 'col-8', + gutter: 0, + } -%} + {%- if _date is not empty and _image is empty -%} + {%- set _horizontal_grid = { + left_col_classes: 'bcl-card-start-col', + right_col_classes: 'col', + gutter: 0, + } -%} + {%- endif -%} +{%- endif -%} + +{%- if _variant == 'search' -%} + {%- set _left_col_classes = 'bcl-card-start-col mb-3 mb-lg-0 bcl-size-small' -%} + {%- set _right_col_classes = 'col-12 col-md' -%} + {%- set _image_classes = '' -%} + + {# Backward compatibility setting: image is hidden in mobile viewports. #} + {%- if bc_card_search_image_hide_on_mobile -%} + {%- set _image_classes = 'd-none d-md-block' -%} + {%- endif -%} + + {# Backward compatibility setting: card uses grid classes. #} + {%- if bc_card_search_use_grid_classes -%} + {%- set _left_col_classes = 'col-md-3 col-lg-4 col-xl-2 rounded mw-listing-img mb-md-0 mb-3' -%} + {%- set _right_col_classes = 'col-md-9 col-lg-8 col-xl-10' -%} + {%- endif -%} + {%- if _image.classes is not empty and _image_classes is not empty -%} + {%- set _image_classes = _image.classes ~ ' ' ~ _image_classes -%} + {%- elseif _image.classes is not empty -%} + {%- set _image_classes = _image.classes -%} + {%- endif -%} + + {%- include '@oe-bcl/card' with { + horizontal: true, + horizontal_grid: { + left_col_classes: _left_col_classes, + right_col_classes: _right_col_classes, + }, + extra_classes_body: 'p-0 pb-md-0 pb-3', + badges: _badges, + title: title, + title_tag: title_tag, + title_link: _title_link, + title_attributes: title_attributes, + image_copyright: image_copyright, + text: (text is not empty) ? { + content: text, + classes: 'mt-2', + tag: 'div', + } : {}, + attributes: _attributes.addClass([ + 'listing-item', + 'border-bottom', + 'border-md-0', + 'border-0', + 'mb-3', + ]), + text_color: 'dark', + image: (_image is not empty) ? _image|merge({ + position: _image.position|default('top'), + classes: _image_classes, + }) : {}, + date: _date, + content: _content, + } only -%} +{%- else -%} + {%- include '@oe-bcl/card' with { + horizontal: _orientation == 'horizontal', + horizontal_grid: _horizontal_grid, + title: title, + title_link: _title_link, + title_tag: title_tag, + title_attributes: title_attributes, + image_copyright: image_copyright, + image_copyright_label: 'Image credit:'|t, + subtitle: (subtitle is not empty) ? { + content: subtitle, + } : {}, + text: (text is not empty) ? { + content: text, + classes: 'mt-2', + tag: 'div', + } : {}, + content: _content, + image: _image, + date: _date, + card_header: header, + card_footer: footer, + badges: _badges, + attributes: _attributes, + } only -%} +{%- endif -%} diff --git a/components/card_layout/card_layout.component.yml b/components/card_layout/card_layout.component.yml new file mode 100644 index 000000000..b0e0ef8a1 --- /dev/null +++ b/components/card_layout/card_layout.component.yml @@ -0,0 +1,155 @@ +name: 'Card layout' +description: 'Bootstrap card layout for rendering a series of cards as a group or grid layout.' +props: + type: object + properties: + variant: + title: Variant + type: string + description: 'The card layout variant. Defaults to group.' + enum: + - group + - grid + - masonry + 'meta:enum': + group: 'Group (default)' + grid: Grid + masonry: Masonry + items: + title: 'Card items' + type: array + description: 'An array of card pattern items.' + items: + type: object + properties: + title: + title: Title + type: [string, array, object] + title_tag: + title: 'Title tag' + type: string + subtitle: + title: Subtitle + type: [string, array, object] + text: + title: Text + type: [string, array, object] + image: + title: Image + type: object + properties: + src: + type: string + alt: + type: string + position: + type: string + classes: + type: string + rounded: + type: string + image_copyright: + title: 'Image copyright' + type: [string, array, object] + content: + title: Content + type: [string, array, object] + card_header: + title: 'Card header' + type: [string, array, object] + card_footer: + title: 'Card footer' + type: [string, array, object] + badges: + title: Badges + type: array + description: 'List of badges for the card.' + items: + type: [string, object] + properties: + label: + type: [string, array, object] + background: + type: string + url: + type: string + title: + type: string + dismissible: + type: boolean + assistive_text: + type: string + rounded_pill: + type: boolean + outline: + type: boolean + icon_path: + type: string + attributes: + type: object + attributes: + title: Attributes + type: object + grid_columns: + title: 'Grid columns' + type: integer + description: 'Number of grid columns. Defaults to 1.' + enum: + - 1 + - 2 + - 3 + 'meta:enum': + 1: '1 (default)' + 2: '2' + 3: '3' + responsiveness: + title: Responsiveness + type: string + description: 'Breakpoint used for the responsive columns. Defaults to md.' + enum: + - sm + - md + - lg + - xl + - xxl + 'meta:enum': + sm: sm + md: 'md (default)' + lg: lg + xl: xl + xxl: xxl + responsive_columns: + title: 'Responsive columns' + type: integer + description: 'Number of responsive columns.' + enum: + - 2 + - 3 + 'meta:enum': + 2: '2' + 3: '3' + gutter: + title: Gutter + type: integer + description: 'Gutter between cards.' + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + 'meta:enum': + 0: '0' + 1: '1' + 2: '2' + 3: '3' + 4: '4' + 5: '5' + equal_height: + title: 'Equal height' + type: boolean + description: 'If true, all cards will have the same height. Defaults to false.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/card_layout/card_layout.story.twig b/components/card_layout/card_layout.story.twig new file mode 100644 index 000000000..a0672dd2d --- /dev/null +++ b/components/card_layout/card_layout.story.twig @@ -0,0 +1,152 @@ +{% set simple_items = [ + { + title: 'Card title 1', + title_tag: 'h2', + text: 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.', + card_footer: 'Footer of card 1', + }, + { + title: 'Card title 2', + text: 'This card has supporting text below as a natural lead-in to additional content.', + card_footer: 'Footer of card 2', + }, + { + title: 'Card title 3', + text: 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.', + card_footer: 'Footer of card 3', + }, +] %} + +{% set masonry_items = simple_items|merge([ + { + title: 'Card title 4', + text: 'A compact card with a short supporting sentence.', + card_footer: 'Footer of card 4', + }, + { + title: 'Card title 5', + text: 'This card contains a medium amount of supporting text. Its different height helps demonstrate how cards are placed in the available vertical space.', + card_footer: 'Footer of card 5', + }, + { + title: 'Card title 6', + text: 'Another short card.', + card_footer: 'Footer of card 6', + }, +]) %} + +{% set image_items = [ + { + title: 'Title card 1', + subtitle: 'Subtitle card 1', + text: 'Text card with longer text which is supported 1', + image: { + src: 'https://picsum.photos/1000/500/', + alt: 'Alternative text for card image', + }, + content: { + '#type': 'html_tag', + '#tag': 'button', + '#value': 'Custom action', + '#attributes': { + class: [ + 'btn', + 'btn-primary', + ], + }, + }, + card_header: 'Header of card 1', + card_footer: 'Footer of card 1', + }, + { + title: 'Title card 2', + subtitle: 'Subtitle card 2', + text: 'Text card with longer text which is supported 2', + image: { + src: 'https://picsum.photos/1000/500/', + alt: 'Alternative text for card image', + }, + content: 'Custom test', + card_header: 'Header of card 2', + card_footer: 'Footer of card 2', + }, + { + title: 'Title card 3', + subtitle: 'Subtitle card 3', + text: 'Text card with longer text which is supported 3', + image: { + src: 'https://picsum.photos/1000/500/', + alt: 'Alternative text for card image', + }, + content: 'Some representative placeholder content for the first card.', + card_header: 'Header of card 3', + card_footer: 'Footer of card 3', + }, +] %} + +
+
+

Group

+ {% include 'oe_bootstrap_theme:card_layout' with { + variant: 'group', + items: simple_items, + } only %} +
+ +
+

Grid

+ {% include 'oe_bootstrap_theme:card_layout' with { + variant: 'grid', + items: image_items, + grid_columns: 2, + responsiveness: 'md', + responsive_columns: 2, + gutter: 2, + equal_height: true, + } only %} +
+ +
+

Masonry

+ {% include 'oe_bootstrap_theme:card_layout' with { + variant: 'masonry', + items: masonry_items, + grid_columns: 1, + responsiveness: 'md', + responsive_columns: 3, + gutter: 3, + } only %} +

+ Masonry requires its JavaScript plugin to be added manually. See the + Bootstrap Masonry example + for details. +

+
+
+ + + diff --git a/components/card_layout/card_layout.twig b/components/card_layout/card_layout.twig new file mode 100644 index 000000000..11d306769 --- /dev/null +++ b/components/card_layout/card_layout.twig @@ -0,0 +1,16 @@ +{# +/** + * @file + * Card layout component. + */ +#} +{% include '@oe-bcl/card-layout' with { + type: variant|default('group'), + items: items|default([])|bcl_card_list, + grid_columns: grid_columns, + responsive_columns: responsive_columns, + responsiveness: responsiveness, + gutter: gutter, + equal_height: variant|default('group') == 'group' ? false : equal_height, + attributes: attributes, +} only %} diff --git a/components/card_v2/card_v2.component.yml b/components/card_v2/card_v2.component.yml new file mode 100644 index 000000000..77470124f --- /dev/null +++ b/components/card_v2/card_v2.component.yml @@ -0,0 +1,62 @@ +name: 'Card V2' +description: 'Bootstrap 5 card component in its second iteration. Every zone is exposed as a render prop so plain markup, views, or other components can be placed inside.' +props: + type: object + properties: + variant: + title: Variant + type: string + description: 'The card variant.' + enum: + - default + - horizontal + 'meta:enum': + default: Default + horizontal: Horizontal + tag: + title: 'HTML wrapper tag' + type: string + description: 'Semantic element for the outer wrapper. Defaults to article.' + enum: + - article + - section + - div + 'meta:enum': + article: '
(default)' + section: '
' + div: '
' + title: + title: Title + type: [string, array, object] + description: 'Card title. Supports markup, e.g. for links.' + media: + title: Media + type: [string, array, object] + description: 'Optional. Accepts an image, video, icon, map thumbnail, or another component.' + content: + title: 'Body content' + type: [string, array, object] + description: 'Card body content.' + header: + title: Header + type: [string, array, object] + description: 'Optional meta information or category content.' + footer: + title: Footer + type: [string, array, object] + description: 'Optional secondary actions or metadata.' + attributes: + title: 'Card attributes' + type: Drupal\Core\Template\Attribute + media_attributes: + title: 'Media attributes' + type: Drupal\Core\Template\Attribute + content_attributes: + title: 'Body attributes' + type: Drupal\Core\Template\Attribute + header_attributes: + title: 'Header attributes' + type: Drupal\Core\Template\Attribute + footer_attributes: + title: 'Footer attributes' + type: Drupal\Core\Template\Attribute diff --git a/components/card_v2/card_v2.story.twig b/components/card_v2/card_v2.story.twig new file mode 100644 index 000000000..e9101debe --- /dev/null +++ b/components/card_v2/card_v2.story.twig @@ -0,0 +1,156 @@ +{% set title %} +

Title card

+{% endset %} + +{% set linked_title %} +

Linked title card

+{% endset %} + +{% set media %} + Alternative text for card image +{% endset %} + +{% set media_horizontal %} + Alternative text for card image +{% endset %} + +{% set content %} +
Subtitle card
+
+ This card includes a longer description to show how body text wraps across multiple lines in the component preview. + Example link +
+
+ OrganisationPosition +
+
+ +
+
+ A link badge + A non-rounded badge + A solid badge +
+{% endset %} + +{% set header %} + Header of card +{% endset %} + +{% set footer %} + Footer of card +{% endset %} + +{% set simple_content %} +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse vel mauris vitae ipsum blandit condimentum ut eget quam.

+{% endset %} + +{% set date_media %} + +{% endset %} + +{% set section_title %} +

Section card

+{% endset %} + +{% set section_content %} +

+ A compact card with badges and supporting content for related resources. +

+
+ Static badge + Clickable style badge + Another badge +
+{% endset %} + +{% set badges_title %} +

Card with badges

+{% endset %} + +{% set badges_content %} +

+ This card contains various badges that can represent categories, filters, tags, or actions. +

+
+ Solid badge + Outlined badge + Clickable success + Static warning +
+{% endset %} + +
+ {% include 'oe_bootstrap_theme:card_v2' with { + variant: 'default', + title: linked_title, + media: media, + content: content, + header: header, + footer: footer, + } only %} + + {% include 'oe_bootstrap_theme:card_v2' with { + variant: 'horizontal', + title: title, + media: media_horizontal, + content: content, + } only %} + + {% include 'oe_bootstrap_theme:card_v2' with { + variant: 'default', + title: title, + content: simple_content, + } only %} + + {% include 'oe_bootstrap_theme:card_v2' with { + variant: 'horizontal', + title: title, + media: date_media, + content: simple_content, + } only %} + + {% include 'oe_bootstrap_theme:card_v2' with { + variant: 'default', + tag: 'section', + attributes: create_attribute({ + class: ['position-relative', 'text-decoration-none'] + }), + title: section_title, + media: media, + content: section_content, + footer: footer, + } only %} + + {% include 'oe_bootstrap_theme:card_v2' with { + variant: 'default', + title: badges_title, + media: media, + content: badges_content, + } only %} + + {% include 'oe_bootstrap_theme:card_v2' with { + variant: 'default', + title: title, + media: media, + content: simple_content, + header: header, + footer: footer, + media_attributes: create_attribute().addClass(['bg-info-subtle']), + content_attributes: create_attribute().addClass(['bg-warning-subtle']), + header_attributes: create_attribute().addClass(['bg-primary-subtle']), + footer_attributes: create_attribute().addClass(['bg-success-subtle']), + } only %} +
diff --git a/components/card_v2/card_v2.twig b/components/card_v2/card_v2.twig new file mode 100644 index 000000000..8caefdd75 --- /dev/null +++ b/components/card_v2/card_v2.twig @@ -0,0 +1,43 @@ +{# +/** + * @file + * Card V2 component. + */ +#} +{% set _tag = tag|default('article') %} +{% set _attributes = attributes|default(create_attribute()) %} +{% set _media_attributes = media_attributes|default(create_attribute()) %} +{% set _content_attributes = content_attributes|default(create_attribute()) %} +{% set _header_attributes = header_attributes|default(create_attribute()) %} +{% set _footer_attributes = footer_attributes|default(create_attribute()) %} +{% set _classes = ['card', 'v2'] %} + +{% if variant|default('default') == 'horizontal' %} + {% set _classes = _classes|merge(['horizontal']) %} +{% endif %} + +<{{ _tag }}{{ _attributes.addClass(_classes) }}> + {% if media is not empty %} + + {{ media }} +
+ {% endif %} + {% if header is not empty %} + + {{ header }} + + {% endif %} + + {% if title is not empty %} +
+ {{ title }} +
+ {% endif %} + {{ content }} + + {% if footer is not empty %} + + {{ footer }} + + {% endif %} + diff --git a/components/carousel/carousel.component.yml b/components/carousel/carousel.component.yml new file mode 100644 index 000000000..1a3a6bf85 --- /dev/null +++ b/components/carousel/carousel.component.yml @@ -0,0 +1,112 @@ +name: Carousel +description: 'A slideshow component for cycling through elements, like images or slides of text.' +props: + type: object + properties: + title: + title: Title + type: [string, array, object] + description: 'Title of the carousel.' + title_tag: + title: 'Title tag' + type: string + description: 'The tag to use for the title. Defaults to h2.' + carousel_id: + title: 'Carousel ID' + type: string + description: 'Unique ID used to connect carousel controls and indicators.' + slides: + title: Slides + type: array + description: 'Slides shown in the carousel.' + items: + type: object + properties: + image: + title: Image + type: object + description: 'Image attributes used to build the slide image.' + properties: + src: + type: string + alt: + type: string + title: + type: string + loading: + type: string + width: + type: string + height: + type: string + class: + type: [string, array, object] + caption_title: + title: 'Caption title' + type: [string, array, object] + caption: + title: Caption + type: [string, array, object] + caption_classes: + title: 'Caption classes' + type: string + interval: + title: Interval + type: integer + copyright: + title: Copyright + type: [string, array, object] + link: + title: Link + type: object + properties: + label: + type: [string, array, object] + path: + type: string + icon_position: + type: string + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + icon_path: + title: 'Icons path' + type: string + description: 'Path to icons.' + active_item: + title: 'Active item' + type: integer + description: 'The 1-based index of the active slide. Defaults to 1.' + fade: + title: Fade + type: boolean + description: 'Animate slides with a fade transition.' + show_controls: + title: 'Show controls' + type: boolean + description: 'Show previous and next controls. Defaults to true.' + show_indicators: + title: 'Show indicators' + type: boolean + description: 'Show slide indicators. Defaults to false.' + autoplay: + title: Autoplay + type: boolean + description: 'Whether to initialise and animate the carousel at page load. Defaults to true.' + disable_touch: + title: 'Disable touch' + type: boolean + description: 'Disable touch swiping.' + dark_mode: + title: 'Dark mode' + type: boolean + description: 'Use dark carousel controls and indicators.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/carousel/carousel.story.twig b/components/carousel/carousel.story.twig new file mode 100644 index 000000000..0bbb31982 --- /dev/null +++ b/components/carousel/carousel.story.twig @@ -0,0 +1,98 @@ +{% set slides = [ + { + image: { + src: 'https://picsum.photos/seed/oe-carousel-sdc-1/1000/500', + alt: 'Modern building with glass facade' + }, + caption_title: 'Research and innovation', + caption: 'Programmes and initiatives supporting sustainable growth across Europe.', + interval: 5000, + copyright: '© 2026 Example Organisation', + link: { + label: 'Read more', + path: 'https://example.com/research' + } + }, + { + image: { + src: 'https://picsum.photos/seed/oe-carousel-sdc-2/1000/500', + alt: 'People walking through a public square' + }, + caption_title: 'Digital services', + caption: 'Accessible public services designed around the needs of people and businesses.', + interval: 4000, + copyright: '© 2026 Example Organisation' + }, + { + image: { + src: 'https://picsum.photos/seed/oe-carousel-sdc-3/1000/500', + alt: 'Coastal landscape at sunset' + }, + caption_title: 'Climate action', + caption: 'Policies and projects that help communities adapt and reduce emissions.', + interval: 4000, + link: { + label: 'View priorities', + path: 'https://example.com/climate' + } + } +] %} + +{% set rich_slides = [ + { + image: { + src: 'https://picsum.photos/seed/oe-carousel-sdc-4/1000/500', + alt: 'Conference room with a large presentation screen', + title: 'Digital policy presentation', + loading: 'lazy', + width: '1000', + height: '500', + class: ['object-fit-cover'] + }, + caption_title: 'Interoperable services', + caption: 'Reusable building blocks that help administrations deliver reliable online services.', + interval: 3000, + link: { + label: 'Explore services', + path: 'https://example.com/services', + icon_position: 'after', + icon: { + name: 'chevron-right' + } + } + }, + { + image: { + src: 'https://picsum.photos/seed/oe-carousel-sdc-5/1000/500', + alt: 'Solar panels in a green landscape' + }, + caption_title: 'Sustainable investment', + caption: 'Funding opportunities for climate, infrastructure, and digital transformation.', + interval: 3000 + } +] %} + +
+ {% include 'oe_bootstrap_theme:carousel' with { + title: 'Featured priorities', + title_tag: 'h2', + carousel_id: 'carousel-sdc-preview', + fade: true, + show_controls: true, + show_indicators: true, + autoplay: false, + slides: slides, + } only %} + + {% include 'oe_bootstrap_theme:carousel' with { + title: 'Dark carousel with active item', + title_tag: 'h2', + carousel_id: 'carousel-sdc-dark-preview', + show_controls: true, + show_indicators: true, + autoplay: false, + dark_mode: true, + active_item: 2, + slides: rich_slides, + } only %} +
diff --git a/components/carousel/carousel.twig b/components/carousel/carousel.twig new file mode 100644 index 000000000..c72bd9889 --- /dev/null +++ b/components/carousel/carousel.twig @@ -0,0 +1,50 @@ +{# +/** + * @file + * Carousel component. + */ +#} +{% set icon_path = icon_path|default(bcl_icon_path()) %} +{% set carousel_id = carousel_id|default('carousel'|clean_unique_id) %} +{% set items = [] %} + +{% for slide in slides|default([]) %} + {% set image %} + {% if slide.image is defined and slide.image is not empty %} + + {% endif %} + {% endset %} + + {% set item = slide|merge({ image: image }) %} + {% if slide.link is defined and slide.link is not empty %} + {% set item = item|merge({ + link: slide.link|merge({ + icon_position: slide.link.icon_position|default('before'), + icon: slide.link.icon|default({})|merge({ + name: slide.link.icon.name|default('link'), + path: slide.link.icon.path|default(icon_path), + }), + }), + }) %} + {% endif %} + + {% set items = items|merge([item]) %} +{% endfor %} + +{% include '@oe-bcl/carousel' with { + copyright_label: 'Image credit:'|t, + title: title, + title_tag: title_tag|default('h2'), + id: carousel_id, + autoplay: autoplay ?? true, + dark: dark_mode, + active_item: active_item, + items: items, + fade: fade ?? false, + with_controls: show_controls ?? true, + prev_label: 'Previous'|t, + next_label: 'Next'|t, + with_indicators: show_indicators ?? false, + disable_touch: disable_touch ?? false, + attributes: attributes, +} only %} diff --git a/components/columns/columns.component.yml b/components/columns/columns.component.yml new file mode 100644 index 000000000..c707af3f0 --- /dev/null +++ b/components/columns/columns.component.yml @@ -0,0 +1,20 @@ +name: Columns +description: 'Renders content organised in columns of equal width.' +props: + type: object + properties: + items: + title: Items + type: array + description: 'The items to organize in columns. Accepts any render array.' + columns: + title: Columns + type: integer + description: 'The number of columns. Defaults to 4.' + tag: + title: Tag + type: string + description: 'HTML tag to use for the wrapper. Defaults to div.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/columns/columns.story.twig b/components/columns/columns.story.twig new file mode 100644 index 000000000..c649e58c1 --- /dev/null +++ b/components/columns/columns.story.twig @@ -0,0 +1,47 @@ +{% set items = [ + { '#plain_text': 'Column 1' }, + { '#plain_text': 'Column 2' }, + { '#plain_text': 'Column 3' }, + { '#plain_text': 'Column 4' }, + { '#plain_text': 'Column 5' }, + { '#plain_text': 'Column 6' }, +] %} + + + +
+
+

Default columns

+ {% include 'oe_bootstrap_theme:columns' with { + items: items, + } only %} +
+ +
+

Three columns

+ {% include 'oe_bootstrap_theme:columns' with { + items: items, + columns: 3, + } only %} +
+ +
+

Section wrapper

+ {% include 'oe_bootstrap_theme:columns' with { + items: items, + columns: 6, + tag: 'section', + } only %} +
+
diff --git a/components/columns/columns.twig b/components/columns/columns.twig new file mode 100644 index 000000000..924d50994 --- /dev/null +++ b/components/columns/columns.twig @@ -0,0 +1,27 @@ +{# +/** + * @file + * Columns component. + */ +#} +{% set tag = tag|default('div') %} +{% set columns = columns|default(4) %} +{% set attributes = attributes|default(create_attribute()) %} +{% + set wrapper_classes = [ + 'columns', + ] +%} +{% + set responsive_grid_styles = [ + '--columns-grid--column-count: ' ~ columns ~ ';', + ] +%} + +<{{ tag }}{{ attributes.addClass(wrapper_classes).setAttribute('style', responsive_grid_styles|join()) }}> + {% for item in items|default([])|element_children %} +
+ {{- item -}} +
+ {% endfor %} + diff --git a/components/content_banner/content_banner.component.yml b/components/content_banner/content_banner.component.yml new file mode 100644 index 000000000..f4148e221 --- /dev/null +++ b/components/content_banner/content_banner.component.yml @@ -0,0 +1,212 @@ +name: 'Content banner' +description: 'Shows the content summary along with the title, image and badges.' +props: + type: object + properties: + background: + title: Background + type: string + description: 'Sets a gray or white background. Defaults to white.' + default: white + enum: + - white + - gray + 'meta:enum': + white: 'White (default)' + gray: Gray + image: + title: Image + type: object + description: 'Image displayed in the content banner.' + properties: + src: + type: string + alt: + type: string + copyright: + title: Copyright + type: [string, array, object] + description: 'Optional copyright text displayed below the image.' + copyright_label: + title: 'Copyright label' + type: string + description: 'Accessible label shown before the copyright. Defaults to Image credit:.' + image_size: + title: 'Image size' + type: string + description: 'The size of the image. Defaults to lg.' + default: lg + enum: + - md + - lg + - xl + 'meta:enum': + md: Medium + lg: 'Large (default)' + xl: 'Extra large' + title: + title: Title + type: [string, array, object] + description: 'Title of the content banner.' + title_tag: + title: 'Title tag' + type: string + description: 'The tag to use for the title. Defaults to h1.' + title_link: + title: 'Title link' + type: object + description: 'Optional link object for the title.' + properties: + label: + type: [string, array, object] + path: + type: string + variant: + type: string + id: + type: string + disabled: + type: boolean + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + icon_position: + type: string + icon_display: + type: string + remove_icon_spacers: + type: boolean + attributes: + type: object + title_attributes: + title: 'Title attributes' + type: Drupal\Core\Template\Attribute + action_bar: + title: 'Action bar' + type: [string, array, object] + description: 'Action bar region, for example action buttons.' + content: + title: Content + type: [string, array, object] + description: 'Text with the summary of the content.' + subtitle: + title: Subtitle + type: object + description: 'Optional card subtitle.' + properties: + content: + type: [string, array, object] + tag: + type: string + classes: + type: string + text: + title: Text + type: object + description: 'Optional card text.' + properties: + content: + type: [string, array, object] + tag: + type: string + classes: + type: string + meta: + title: Meta + type: array + description: 'Miscellaneous information such as date or author.' + items: + type: [string, array, object] + badges: + title: Badges + type: array + description: 'List of badges.' + items: + type: object + properties: + label: + type: [string, array, object] + background: + type: string + url: + type: string + title: + type: string + dismissible: + type: boolean + assistive_text: + type: string + rounded_pill: + type: boolean + outline: + type: boolean + icon_path: + type: string + attributes: + type: object + links: + title: Links + type: array + description: 'List of links.' + items: + type: object + properties: + label: + type: [string, array, object] + path: + type: string + variant: + type: string + id: + type: string + disabled: + type: boolean + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + icon_position: + type: string + icon_display: + type: string + remove_icon_spacers: + type: boolean + attributes: + type: object + date: + title: 'Date block' + type: object + description: 'Date block displayed in place of an image.' + properties: + day: + type: [string, integer] + month: + type: string + date_time: + type: string + year: + type: [string, integer] + end_day: + type: [string, integer] + end_month: + type: string + end_year: + type: [string, integer] + icon_path: + type: string + attributes: + type: object + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/content_banner/content_banner.story.twig b/components/content_banner/content_banner.story.twig new file mode 100644 index 000000000..bbeab1736 --- /dev/null +++ b/components/content_banner/content_banner.story.twig @@ -0,0 +1,99 @@ +{% set image = { + src: 'https://picsum.photos/id/1015/1200/600', + alt: 'Mountain landscape reflected in a lake', +} %} + +{% set badges = [ + { + label: 'Primary badge', + rounded_pill: true, + outline: false, + }, + { + label: 'Secondary badge', + rounded_pill: true, + }, +] %} + +{% set links = [ + { + label: 'Read the latest updates', + path: '/example-1', + }, + { + label: 'View related policies', + path: '/example-2', + }, +] %} + +{% set action_bar %} +
+ {% include 'oe_bootstrap_theme:button' with { + label: 'Subscribe', + variant: 'primary', + type: 'button', + } only %} +
+{% endset %} + +
+
+

White background with image

+ {% include 'oe_bootstrap_theme:content_banner' with { + background: 'white', + image: image, + copyright: '© 2026 Example Organisation', + copyright_label: 'Photo credit:', + title: 'Building a greener and more resilient Europe', + title_tag: 'h2', + subtitle: { + content: 'Climate and energy', + tag: 'div', + classes: 'fs-5', + }, + text: { + content: 'Supporting practical action across regions and sectors.', + tag: 'p', + classes: 'mt-2', + }, + meta: ['23 July 2026', 'Policy update'], + content: 'Discover the latest initiatives supporting clean energy, sustainable industry and stronger communities across Europe.', + badges: badges, + links: links, + action_bar: action_bar, + } only %} +
+ +
+

Gray background with extra large image

+ {% include 'oe_bootstrap_theme:content_banner' with { + background: 'gray', + image: image, + image_size: 'xl', + title: 'A shared plan for sustainable growth', + content: 'Investment and cooperation are helping regions deliver practical solutions for citizens and businesses.', + } only %} +
+ +
+

Gray background without image

+ {% include 'oe_bootstrap_theme:content_banner' with { + background: 'gray', + title: 'Key figures and programme highlights', + meta: ['Updated 23 July 2026', '6 min read'], + content: 'An overview of current priorities, milestones and expected results.', + badges: badges, + } only %} +
+ +
+

White background with medium image

+ {% include 'oe_bootstrap_theme:content_banner' with { + background: 'white', + image: image, + image_size: 'md', + title: 'Regional projects making a difference', + content: 'Local projects are turning shared priorities into measurable improvements.', + } only %} +
+
diff --git a/components/content_banner/content_banner.twig b/components/content_banner/content_banner.twig new file mode 100644 index 000000000..d185041d8 --- /dev/null +++ b/components/content_banner/content_banner.twig @@ -0,0 +1,98 @@ +{# +/** + * @file + * Content banner component. + */ +#} +{%- set _content -%} + {%- set content_attributes = create_attribute().addClass(['mt-4', 'card-content']) -%} + {%- if meta is not empty -%} + {%- set content_attributes = create_attribute().addClass('card-content') -%} +
+ {%- for meta_item in meta -%} + {%- if meta_item is not empty -%} + {{- meta_item -}} + {%- endif -%} + {%- endfor -%} +
+ {%- endif -%} + {%- if content is not empty -%} + {{- content -}} + {%- endif -%} +{%- endset -%} + +{%- set _badges = [] -%} +{%- for badge in badges|default([]) -%} + {%- set _badge = { + outline: true, + }|merge(badge) -%} + {%- set _badge = _badge|merge({ + attributes: create_attribute(_badge.attributes|default({})), + }) -%} + {%- set _badges = _badges|merge([_badge]) -%} +{%- endfor -%} + +{%- set _links = [] -%} +{%- for link in links|default([]) -%} + {%- set _link = link|merge({ + attributes: create_attribute(link.attributes|default({})), + }) -%} + {%- if _link.icon is defined and _link.icon is not empty and _link.icon.path is not defined -%} + {%- set _link = _link|merge({ + icon: _link.icon|merge({path: bcl_icon_path()}), + }) -%} + {%- endif -%} + {%- set _links = _links|merge([_link]) -%} +{%- endfor -%} + +{%- set _title_link = title_link|default({}) -%} +{%- if _title_link is not empty -%} + {%- set _title_link = _title_link|merge({ + attributes: create_attribute(_title_link.attributes|default({})), + }) -%} + {%- if _title_link.icon is defined and _title_link.icon is not empty and _title_link.icon.path is not defined -%} + {%- set _title_link = _title_link|merge({ + icon: _title_link.icon|merge({path: bcl_icon_path()}), + }) -%} + {%- endif -%} +{%- endif -%} + +{%- set _image = {} -%} +{%- if image is not empty -%} + {%- set _image = image|merge({ + path: image.src|default(''), + alt: image.alt|default(''), + }) -%} +{%- endif -%} + +{%- set _date = date|default({}) -%} +{%- if _date is not empty -%} + {%- set _date = _date|merge({ + attributes: create_attribute(_date.attributes|default({})), + }) -%} + {%- if _date.icon_path is not defined or _date.icon_path is empty -%} + {%- set _date = _date|merge({ + icon_path: bcl_icon_path(), + }) -%} + {%- endif -%} +{%- endif -%} + +{%- include '@oe-bcl/bcl-content-banner/bcl-content-banner.html.twig' with { + background: background|default('white'), + title: title, + title_tag: title_tag|default('h1'), + title_link: _title_link, + title_attributes: title_attributes, + subtitle: subtitle, + text: text, + content: _content, + image: _image, + date: _date, + copyright: copyright, + copyright_label: copyright_label|default('Image credit:'|t), + image_size: image_size|default('lg'), + badges: _badges, + links: _links, + action_bar: action_bar, + attributes: attributes, +} only -%} diff --git a/components/date_block/date_block.component.yml b/components/date_block/date_block.component.yml new file mode 100644 index 000000000..8d6abb0c2 --- /dev/null +++ b/components/date_block/date_block.component.yml @@ -0,0 +1,40 @@ +name: 'Date block' +description: 'Date block allows to present a date that can be placed in other components.' +props: + type: object + properties: + day: + title: Day + type: [string, integer] + description: 'Day displayed in the date block.' + month: + title: Month + type: string + description: 'Month displayed in the date block.' + date_time: + title: Datetime + type: string + description: 'Machine-readable date added to the datetime attribute.' + year: + title: Year + type: [string, integer] + description: 'Year displayed in the date block.' + end_day: + title: 'End day' + type: [string, integer] + description: 'End day displayed for a date range.' + end_month: + title: 'End month' + type: string + description: 'End month displayed for a date range.' + end_year: + title: 'End year' + type: [string, integer] + description: 'End year displayed for a date range.' + icon_path: + title: 'Icons path' + type: string + description: 'Path to the SVG icon file. Defaults to the BCL icon path.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/date_block/date_block.story.twig b/components/date_block/date_block.story.twig new file mode 100644 index 000000000..1e3c20b06 --- /dev/null +++ b/components/date_block/date_block.story.twig @@ -0,0 +1,57 @@ +
+
+

Explicit date values

+
+ {% include 'oe_bootstrap_theme:date_block' with { + day: 23, + month: 'Jul', + year: 2026, + date_time: '2026-07-23', + attributes: create_attribute().addClass('custom-date-block'), + } only %} +
+
+ +
+

Same-month date range

+
+ {% include 'oe_bootstrap_theme:date_block' with { + day: 23, + month: 'Jan', + year: 2026, + date_time: '2026-01-23', + end_day: 30, + } only %} +
+
+ +
+

Same-year date range

+
+ {% include 'oe_bootstrap_theme:date_block' with { + day: 23, + month: 'Jan', + year: 2026, + date_time: '2026-01-23', + end_day: 11, + end_month: 'Feb', + } only %} +
+
+ +
+

Cross-year date range

+
+ {% include 'oe_bootstrap_theme:date_block' with { + day: 23, + month: 'Jan', + year: 2026, + date_time: '2026-01-23', + end_day: 11, + end_month: 'Feb', + end_year: 2027, + icon_path: bcl_icon_path(), + } only %} +
+
+
diff --git a/components/date_block/date_block.twig b/components/date_block/date_block.twig new file mode 100644 index 000000000..d58eebe64 --- /dev/null +++ b/components/date_block/date_block.twig @@ -0,0 +1,17 @@ +{# +/** + * @file + * Date block component. + */ +#} +{% include '@oe-bcl/date-block' with { + day: day, + month: month, + date_time: date_time, + year: year, + end_day: end_day, + end_month: end_month, + end_year: end_year, + icon_path: icon_path|default(bcl_icon_path()), + attributes: attributes, +} only %} diff --git a/components/description_list/description_list.component.yml b/components/description_list/description_list.component.yml new file mode 100644 index 000000000..8ba4cd228 --- /dev/null +++ b/components/description_list/description_list.component.yml @@ -0,0 +1,81 @@ +name: 'Description list' +description: 'Description lists are used to display and organise content that belongs to a category with a descriptions. Common use cases include glossary type lists or a list of speakers with their biographies.' +props: + type: object + properties: + title: + title: Title + type: [string, array, object] + description: 'Title of the description list.' + title_tag: + title: 'Title tag' + type: string + description: 'The tag to use for the title. Defaults to h2.' + items: + title: 'List items' + type: array + description: 'Each item is composed by one or more terms and one or more definitions. Each term entry can have an icon.' + items: + type: object + properties: + term: + title: 'Terms' + description: 'A string or an array of term objects.' + anyOf: + - type: string + - type: array + items: + type: object + properties: + label: + title: Label + type: [string, array, object] + description: 'Label text or render array.' + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + definition: + title: 'Definition' + description: 'A string or an array of definition objects.' + anyOf: + - type: string + - type: array + items: + type: object + properties: + label: + type: [string, array, object] + icon_path: + type: string + title: "Icons path" + description: "Path to icons" + orientation: + title: Orientation + type: string + description: 'Orientation of the list.' + enum: + - default + - horizontal + 'meta:enum': + default: Default + horizontal: Horizontal + bordered: + title: Bordered + type: boolean + description: 'Adds a border to the description list.' + col_classes: + title: 'Column classes' + type: string + description: 'Classes for the columns.' + title_attributes: + title: 'Title attributes' + type: Drupal\Core\Template\Attribute + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/description_list/description_list.story.twig b/components/description_list/description_list.story.twig new file mode 100644 index 000000000..7da880ac0 --- /dev/null +++ b/components/description_list/description_list.story.twig @@ -0,0 +1,41 @@ +{% include 'oe_bootstrap_theme:description_list' with { + title: 'This is the title of this description list.', + items: [ + { + term: [ + { + label: 'Single label with icon', + icon: { name: 'geo-alt-fill' } + } + ], + definition: 'Description of first term text goes here.' + }, + { + term: [ + { label: 'First label without icon' }, + { label: 'Second label', icon: { name: 'upload' } } + ], + definition: 'Description of second term text goes here.' + }, + { + term: [ + { label: [{ '#markup': 'A label with some markup.' }] } + ], + definition: [ + { label: 'First description of third term text goes here.' }, + { label: 'Second description of third term text goes here.' } + ] + }, + { + term: [ + { label: 'Custom icon size', icon: { name: 'calendar-x', size: 'xl' } } + ], + definition: 'This is a bigger icon.' + }, + { + term: 'A single term without icon.', + definition: 'Yet another definition text.' + } + ], + orientation: 'horizontal', +} only %} diff --git a/components/description_list/description_list.twig b/components/description_list/description_list.twig new file mode 100644 index 000000000..0b664829b --- /dev/null +++ b/components/description_list/description_list.twig @@ -0,0 +1,17 @@ +{# +/** + * @file + * Description list component. + */ +#} +{% include '@oe-bcl/description-list' with { + title: title, + title_link: title_link, + title_tag: title_tag|default('h2'), + title_attributes: title_attributes, + items: items|bcl_description_list_items(icon_path), + variant: orientation, + bordered: bordered, + col_classes: col_classes, + attributes: attributes, +} only %} diff --git a/components/dropdown/dropdown.component.yml b/components/dropdown/dropdown.component.yml new file mode 100644 index 000000000..7c6c0420a --- /dev/null +++ b/components/dropdown/dropdown.component.yml @@ -0,0 +1,168 @@ +name: Dropdown +description: 'Toggle contextual overlays for displaying lists of links and actions.' +props: + type: object + properties: + button_label: + title: 'Dropdown button label' + type: [string, array, object] + description: 'Label used by the default dropdown toggle.' + dropdown_id: + title: 'Dropdown ID' + type: string + description: 'ID connecting the toggle and menu. A unique ID is generated when omitted.' + trigger: + title: Trigger + type: object + description: 'Optional BCL button or link configuration for the dropdown toggle.' + properties: + label: + type: [string, array, object] + path: + type: [string, object] + variant: + type: string + size: + type: string + type: + type: string + outline: + type: boolean + disabled: + type: boolean + assistive_text: + type: string + standalone: + type: boolean + icon: + anyOf: + - type: string + - type: object + properties: + name: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + attributes: + type: object + link: + title: 'Link trigger' + type: boolean + description: 'Render the dropdown toggle as a link instead of a button.' + direction: + title: Direction + type: string + description: 'Direction in which the menu opens.' + enum: + - dropup + - dropend + - dropstart + 'meta:enum': + dropup: Up + dropend: End + dropstart: Start + alignment: + title: Alignment + type: string + description: 'Dropdown menu alignment.' + enum: + - dropdown-menu-end + - dropdown-menu-sm-end + - dropdown-menu-md-end + - dropdown-menu-lg-end + - dropdown-menu-xl-end + - dropdown-menu-xxl-end + - dropdown-menu-start + - dropdown-menu-sm-start + - dropdown-menu-md-start + - dropdown-menu-lg-start + - dropdown-menu-xl-start + - dropdown-menu-xxl-start + 'meta:enum': + dropdown-menu-end: End + dropdown-menu-sm-end: 'End from sm' + dropdown-menu-md-end: 'End from md' + dropdown-menu-lg-end: 'End from lg' + dropdown-menu-xl-end: 'End from xl' + dropdown-menu-xxl-end: 'End from xxl' + dropdown-menu-start: Start + dropdown-menu-sm-start: 'Start from sm' + dropdown-menu-md-start: 'Start from md' + dropdown-menu-lg-start: 'Start from lg' + dropdown-menu-xl-start: 'Start from xl' + dropdown-menu-xxl-start: 'Start from xxl' + dark_mode: + title: 'Dark mode' + type: boolean + description: 'Use the dark dropdown menu style.' + remove_wrapper: + title: 'Remove wrapper' + type: boolean + description: 'Do not render the dropdown wrapper element.' + inside_navigation: + title: 'Inside navigation' + type: boolean + description: 'Render menu accessibility attributes for use inside navigation.' + items: + title: 'Dropdown items' + type: array + description: 'Links, buttons, and dividers displayed in the menu.' + items: + type: object + properties: + label: + type: [string, array, object] + path: + type: [string, object] + button: + type: boolean + divider: + type: boolean + active: + type: boolean + disabled: + type: boolean + id: + type: string + title: + type: string + type: + type: string + variant: + type: string + size: + type: string + outline: + type: boolean + standalone: + type: boolean + icon: + anyOf: + - type: string + - type: object + properties: + name: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + attributes: + type: object + icon_path: + title: 'Icons path' + type: string + description: 'Path to the SVG icon file. Defaults to the BCL icon path.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/dropdown/dropdown.story.twig b/components/dropdown/dropdown.story.twig new file mode 100644 index 000000000..16f369e3b --- /dev/null +++ b/components/dropdown/dropdown.story.twig @@ -0,0 +1,83 @@ +{% set items = [ + { + label: 'A link', + path: '/example', + }, + { + label: 'A button', + button: true, + }, + { + divider: true, + }, + { + label: 'A disabled button', + button: true, + disabled: true, + }, + { + label: 'An active button', + button: true, + active: true, + }, +] %} + +
+
+

Default

+ {% include 'oe_bootstrap_theme:dropdown' with { + button_label: 'Dropdown toggle', + items: items, + } only %} +
+ +
+

Dark menu aligned to the end

+ {% include 'oe_bootstrap_theme:dropdown' with { + button_label: 'Dark dropdown', + alignment: 'dropdown-menu-end', + dark_mode: true, + items: items, + } only %} +
+ +
+

Dropup

+ {% include 'oe_bootstrap_theme:dropdown' with { + button_label: 'Dropup toggle', + direction: 'dropup', + items: items, + } only %} +
+ +
+

Dropend

+ {% include 'oe_bootstrap_theme:dropdown' with { + button_label: 'Dropend toggle', + direction: 'dropend', + items: items, + } only %} +
+ +
+

Dropstart

+ {% include 'oe_bootstrap_theme:dropdown' with { + button_label: 'Dropstart toggle', + direction: 'dropstart', + items: items, + } only %} +
+ +
+

Link trigger

+ {% include 'oe_bootstrap_theme:dropdown' with { + link: true, + trigger: { + label: 'Linked dropdown', + path: '/example', + icon: 'list', + }, + items: items, + } only %} +
+
diff --git a/components/dropdown/dropdown.twig b/components/dropdown/dropdown.twig new file mode 100644 index 000000000..690b8fca1 --- /dev/null +++ b/components/dropdown/dropdown.twig @@ -0,0 +1,54 @@ +{# +/** + * @file + * Dropdown component. + */ +#} +{% set _icon_path = icon_path|default(bcl_icon_path()) %} +{% set _trigger = trigger|default({}) %} +{% set _trigger = _trigger|merge({ + label: _trigger.label|default(button_label|default('')), + attributes: create_attribute(_trigger.attributes|default({})), +}) %} + +{% if _trigger.icon is defined and _trigger.icon is not empty %} + {% set _trigger_icon = _trigger.icon is iterable ? _trigger.icon : { name: _trigger.icon } %} + {% set _trigger = _trigger|merge({ + icon: _trigger_icon|merge({ + path: _trigger_icon.path|default(_icon_path), + attributes: create_attribute(_trigger_icon.attributes|default({})), + }), + }) %} +{% endif %} + +{% set _items = [] %} +{% for item in items|default([]) %} + {% set _item = item|merge({ + attributes: create_attribute(item.attributes|default({})), + }) %} + + {% if _item.icon is defined and _item.icon is not empty %} + {% set _item_icon = _item.icon is iterable ? _item.icon : { name: _item.icon } %} + {% set _item = _item|merge({ + icon: _item_icon|merge({ + path: _item_icon.path|default(_icon_path), + attributes: create_attribute(_item_icon.attributes|default({})), + }), + }) %} + {% endif %} + + {% set _items = _items|merge([_item]) %} +{% endfor %} + +{% include '@oe-bcl/dropdown' with { + id: dropdown_id|default('dropdown'|clean_unique_id), + trigger: _trigger, + link: link ?? false, + direction: direction, + alignment: alignment, + dark: dark_mode ?? false, + remove_wrapper: remove_wrapper ?? false, + inside_navigation: inside_navigation ?? false, + items: _items, + attributes: attributes, +} only %} diff --git a/components/fact_figures/fact_figures.component.yml b/components/fact_figures/fact_figures.component.yml new file mode 100644 index 000000000..160270e52 --- /dev/null +++ b/components/fact_figures/fact_figures.component.yml @@ -0,0 +1,66 @@ +name: 'Fact and Figures' +description: 'Fact and Figures pattern used for the paragraph of same type.' +props: + type: object + properties: + title: + title: Title + type: [string, array, object] + description: 'Title of the facts and figures section.' + title_tag: + title: 'Title tag' + type: string + description: 'The tag to use for the title. Defaults to h2.' + columns: + title: Columns + type: number + description: 'One, two or three columns layout for responsive. Defaults to 3.' + items: + title: Items + type: array + description: 'List of facts, with icon, title, subtitle and content.' + items: + type: object + properties: + icon: + title: Icon + description: 'Icon name or icon object.' + anyOf: + - type: string + - type: object + properties: + name: + type: string + size: + type: string + path: + type: string + attributes: + type: object + title: + title: Title + type: [string, array, object] + subtitle: + title: Subtitle + type: [string, array, object] + description: + title: Description + type: [string, array, object] + link: + title: Link + type: object + description: 'Additional link to be displayed after facts.' + properties: + label: + type: [string, array, object] + path: + type: [string, object] + attributes: + type: object + icon_path: + title: 'Icons path' + type: string + description: 'Path to icons.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/fact_figures/fact_figures.story.twig b/components/fact_figures/fact_figures.story.twig new file mode 100644 index 000000000..32b8dfadc --- /dev/null +++ b/components/fact_figures/fact_figures.story.twig @@ -0,0 +1,80 @@ +{% set items = [ + { + icon: 'sticky', + title: '1529 JIRA Ticket', + subtitle: 'Jira Tickets', + description: 'Nunc condimentum sapien ut nibh finibus suscipit vitae at justo. Morbi quis odio faucibus, commodo tortor id, elementum liber', + }, + { + icon: 'inboxes', + title: '337 Features', + subtitle: 'Feature tickets', + description: 'Turpis varius congue venenatis, erat dui feugiat felis.', + }, + { + icon: 'shield-check', + title: '107 Tests', + subtitle: 'Test tickets', + description: 'Cras vestibulum efficitur mi, quis porta tellus rutrum ut. Quisque at pulvinar sem.', + }, + { + icon: 'file-earmark-code', + title: '5670 Variants', + subtitle: 'Test variants', + description: 'Aliquam lacinia diam eu sem malesuada, in interdum ante bibendum.', + }, + { + icon: 'inbox', + title: '345 Dev Ticket', + subtitle: 'Jira ticket', + description: 'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis nec lectus tortor.', + }, + { + icon: 'eye', + title: '43 Components', + subtitle: 'Figma components', + description: 'Sed efficitur bibendum rutrum. Nunc feugiat congue augue ac consectetur.', + }, +] %} + +
+
+

One column

+ {% include 'oe_bootstrap_theme:fact_figures' with { + title: 'Fact and figures block', + title_tag: 'h3', + columns: 1, + items: items, + link: { + label: 'Read more', + path: '/example', + }, + } only %} +
+ +
+

Two columns

+ {% include 'oe_bootstrap_theme:fact_figures' with { + title: 'Fact and figures block', + columns: 2, + items: items, + link: { + label: 'Read more', + path: '/example', + }, + } only %} +
+ +
+

Three columns

+ {% include 'oe_bootstrap_theme:fact_figures' with { + title: 'Fact and figures block', + columns: 3, + items: items, + link: { + label: 'Read more', + path: '/example', + }, + } only %} +
+
diff --git a/components/fact_figures/fact_figures.twig b/components/fact_figures/fact_figures.twig new file mode 100644 index 000000000..3a0ad0abc --- /dev/null +++ b/components/fact_figures/fact_figures.twig @@ -0,0 +1,30 @@ +{# +/** + * @file + * Fact figures component. + */ +#} +{% set processed_items = [] %} +{% for item in items|default([]) %} + {% if item.icon is defined and item.icon is not empty and item.icon is not iterable %} + {% set item = item|merge({icon: {name: item.icon}}) %} + {% elseif item.icon is defined and item.icon is not empty %} + {% set item = item|merge({ + icon: item.icon|merge({ + attributes: create_attribute(item.icon.attributes|default({})), + }), + }) %} + {% endif %} + {% set processed_items = processed_items|merge([item]) %} +{% endfor %} + +{% include '@oe-bcl/bcl-fact-figures/fact-figures.html.twig' with { + title: title, + title_tag: title_tag|default('h2'), + items: processed_items, + responsive_columns: columns|default(3), + equal_height: true, + icon_path: icon_path|default(bcl_icon_path()), + link_more: link, + attributes: attributes, +} only %} diff --git a/components/featured_media/featured_media.component.yml b/components/featured_media/featured_media.component.yml new file mode 100644 index 000000000..93c448dc4 --- /dev/null +++ b/components/featured_media/featured_media.component.yml @@ -0,0 +1,227 @@ +name: 'Featured media' +description: 'Presents an image, embedded media or video, optionally alongside descriptive content.' +props: + type: object + properties: + caption: + title: Caption + type: [string, array, object] + description: 'Caption displayed below the media.' + embedded_media: + title: 'Embedded media' + type: [string, array, object] + description: 'Embedded media markup or render array.' + image: + title: Image + type: [string, array, object] + description: 'Rendered image or an object containing image attributes.' + properties: + src: + type: string + alt: + type: string + name: + type: string + title: + type: string + responsive: + type: boolean + loading: + type: string + width: + type: [string, integer] + height: + type: [string, integer] + class: + type: [string, array, object] + poster_image: + title: 'Poster image' + type: string + description: 'Poster image URL for native video.' + sources: + title: 'Video sources' + type: array + description: 'Source files for native video.' + items: + type: object + properties: + type: + type: string + src: + type: string + tracks: + title: 'Video tracks' + type: array + description: 'Caption or subtitle tracks for native video.' + items: + type: object + properties: + kind: + type: string + label: + type: string + src: + type: string + src_lang: + type: string + ratio: + title: Ratio + type: string + description: 'Aspect ratio for embedded media. Defaults to 16x9.' + enum: + - 1x1 + - 4x3 + - 16x9 + - 21x9 + 'meta:enum': + 1x1: '1:1' + 4x3: '4:3' + 16x9: '16:9 (default)' + 21x9: '21:9' + title: + title: Title + type: [string, array, object] + description: 'Title displayed above the featured media.' + title_tag: + title: 'Title tag' + type: string + description: 'Tag used for the title. Defaults to h2.' + title_link: + title: 'Title link' + type: object + description: 'Link applied to the title.' + properties: + label: + type: [string, array, object] + path: + type: [string, object] + id: + type: string + disabled: + type: boolean + icon: + type: [string, object] + properties: + name: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_display: + type: string + enum: + - flex + - inline-flex + remove_icon_spacers: + type: boolean + attributes: + type: object + subtitle: + title: Subtitle + type: [string, array, object] + description: 'Subtitle displayed before the text.' + subtitle_tag: + title: 'Subtitle tag' + type: string + description: 'Tag used for the subtitle. Defaults to h3 with a title and h2 without one.' + text: + title: Text + type: [string, array, object] + description: 'Content displayed alongside the media.' + text_position: + title: 'Text position' + type: string + description: 'Position of the text on medium and larger viewports. Defaults to left.' + enum: + - left + - right + 'meta:enum': + left: 'Left (default)' + right: Right + link: + title: Link + type: object + description: 'Optional link displayed after the text.' + properties: + label: + type: [string, array, object] + path: + type: [string, object] + variant: + type: string + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + id: + type: string + disabled: + type: boolean + standalone: + type: boolean + icon: + type: [string, object] + properties: + name: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_display: + type: string + enum: + - flex + - inline-flex + remove_icon_spacers: + type: boolean + attributes: + type: object + copyright: + title: Copyright + type: [string, array, object] + description: 'Optional copyright text displayed below the media.' + copyright_label: + title: 'Copyright label' + type: string + description: 'Visually hidden label for copyright text.' + content_classes: + title: 'Caption classes' + type: string + description: 'Classes applied to the caption. Defaults to bg-light p-3.' + icon_path: + title: 'Icons path' + type: string + description: 'Path to icons used by links. Defaults to the BCL icon path.' + title_attributes: + title: 'Title attributes' + type: Drupal\Core\Template\Attribute + subtitle_attributes: + title: 'Subtitle attributes' + type: Drupal\Core\Template\Attribute + media_attributes: + title: 'Media attributes' + type: Drupal\Core\Template\Attribute + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/featured_media/featured_media.story.twig b/components/featured_media/featured_media.story.twig new file mode 100644 index 000000000..812131f84 --- /dev/null +++ b/components/featured_media/featured_media.story.twig @@ -0,0 +1,67 @@ +{% set image = { + src: 'https://picsum.photos/1200/600/', + alt: 'A representative landscape', + title: 'Featured landscape', + loading: 'lazy', +} %} + +
+
+

Image with caption

+ {% include 'oe_bootstrap_theme:featured_media' with { + image: image, + caption: 'Media description text goes here.', + copyright: '© 2026 Example Organisation', + } only %} +
+ + {% include 'oe_bootstrap_theme:featured_media' with { + image: image, + title: 'Featured image with text', + title_tag: 'h3', + title_link: { + path: '/example', + }, + subtitle: 'Supporting subtitle', + text: 'This content appears to the left of the featured image and provides additional context.', + text_position: 'left', + link: { + label: 'Read more', + path: '/example', + standalone: true, + icon: 'arrow-right', + }, + caption: 'Media description text goes here.', + } only %} + + {% include 'oe_bootstrap_theme:featured_media' with { + embedded_media: { + '#type': 'html_tag', + '#tag': 'iframe', + '#attributes': { + src: 'https://www.youtube.com/embed/nWpgO1EPO_Y', + title: 'Featured video', + }, + }, + title: 'Embedded media with text', + subtitle: 'Media on the left', + text: 'This example uses a wider aspect ratio and positions its descriptive content on the right.', + text_position: 'right', + ratio: '21x9', + caption: 'Embedded media description.', + } only %} + +
+

Native video

+ {% include 'oe_bootstrap_theme:featured_media' with { + poster_image: 'https://picsum.photos/1200/675/', + sources: [ + { + type: 'video/mp4', + src: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4', + }, + ], + caption: 'Native video with a poster image.', + } only %} +
+
diff --git a/components/featured_media/featured_media.twig b/components/featured_media/featured_media.twig new file mode 100644 index 000000000..fa4890955 --- /dev/null +++ b/components/featured_media/featured_media.twig @@ -0,0 +1,124 @@ +{# +/** + * @file + * Featured media component. + */ +#} +{% set _icon_path = icon_path|default(bcl_icon_path()) %} +{% set _image = image|default('') %} +{% set _link = link|default({}) %} +{% set _title_link = title_link|default({}) %} + +{% if _image is not empty and _image.src is defined %} + {% set _image_attributes = create_attribute({ + src: _image.src, + alt: _image.alt|default(''), + }).addClass('flex-end-100') %} + {% if _image.responsive ?? true %} + {% set _image_attributes = _image_attributes.addClass('img-fluid') %} + {% endif %} + {% if _image.class is defined %} + {% set _image_attributes = _image_attributes.addClass(_image.class) %} + {% endif %} + {% if _image.title|default(_image.name|default('')) is not empty %} + {% set _image_attributes = _image_attributes.setAttribute('title', _image.title|default(_image.name)) %} + {% endif %} + {% for attribute in ['loading', 'width', 'height'] %} + {% if attribute(_image, attribute) is defined and attribute(_image, attribute) is not empty %} + {% set _image_attributes = _image_attributes.setAttribute(attribute, attribute(_image, attribute)) %} + {% endif %} + {% endfor %} + {% set _image %} + + {% endset %} +{% endif %} + +{% if _link is not empty %} + {% if _link.attributes is defined %} + {% set _link = _link|merge({attributes: create_attribute(_link.attributes)}) %} + {% endif %} + {% if _link.icon is defined and _link.icon is not empty %} + {% set _link_icon = _link.icon is iterable ? _link.icon : {name: _link.icon} %} + {% if _link_icon.attributes is defined %} + {% set _link_icon = _link_icon|merge({attributes: create_attribute(_link_icon.attributes)}) %} + {% endif %} + {% set _link = _link|merge({ + icon: _link_icon|merge({ + path: _link_icon.path|default(_icon_path), + }), + }) %} + {% endif %} +{% endif %} + +{% if _title_link is not empty %} + {% if _title_link.attributes is defined %} + {% set _title_link = _title_link|merge({attributes: create_attribute(_title_link.attributes)}) %} + {% endif %} + {% if _title_link.icon is defined and _title_link.icon is not empty %} + {% set _title_link_icon = _title_link.icon is iterable ? _title_link.icon : {name: _title_link.icon} %} + {% if _title_link_icon.attributes is defined %} + {% set _title_link_icon = _title_link_icon|merge({attributes: create_attribute(_title_link_icon.attributes)}) %} + {% endif %} + {% set _title_link = _title_link|merge({ + icon: _title_link_icon|merge({ + path: _title_link_icon.path|default(_icon_path), + }), + }) %} + {% endif %} +{% endif %} + +{% set _media %} + {% include '@oe-bcl/featured-media' with { + embedded_media: embedded_media, + image: _image, + poster_image: poster_image, + sources: sources, + tracks: tracks, + content: caption, + content_classes: content_classes|default('bg-light p-3'), + ratio: ratio|default('16x9'), + copyright: copyright, + copyright_label: copyright_label|default(_image is not empty ? 'Image credit:'|t : 'Copyright:'|t), + attributes: media_attributes, + } only %} +{% endset %} + +{% set _has_text = subtitle is not empty or text is not empty or _link is not empty %} + + {% if _has_text %} + {% set _text_order = text_position|default('left') == 'right' ? 'order-2' : 'order-1' %} + {% set _media_order = text_position|default('left') == 'right' ? 'order-1' : 'order-2' %} + + {% if title is not empty %} + {% include '@oe-bcl/heading' with { + title: title, + title_tag: title_tag|default('h2'), + title_link: _title_link, + attributes: title_attributes|default(create_attribute()).addClass('mb-4'), + } only %} + {% endif %} +
+
+ {% if subtitle is not empty %} + {% set _subtitle_tag = subtitle_tag|default(title is not empty ? 'h3' : 'h2') %} + <{{ _subtitle_tag }}{{ subtitle_attributes|default(create_attribute()).addClass('text-secondary') }}> + {{- subtitle -}} + + {% endif %} + {{- text -}} + {% if _link is not empty %} + {% include '@oe-bcl/link' with _link only %} + {% endif %} +
+
+ {{- _media -}} +
+
+ {% else %} +
+
+ {{- _media -}} +
+
+ {% endif %} + diff --git a/components/file/file.component.yml b/components/file/file.component.yml new file mode 100644 index 000000000..51d47c6ca --- /dev/null +++ b/components/file/file.component.yml @@ -0,0 +1,63 @@ +name: File +description: 'Presents a file available for download, optionally with translations.' +props: + type: object + properties: + title: + title: Title + type: [string, array, object] + description: 'Title displayed above the file.' + title_tag: + title: 'Title tag' + type: string + description: 'Tag used for the title. Defaults to h2.' + title_attributes: + title: 'Title attributes' + type: Drupal\Core\Template\Attribute + link_label: + title: 'Download link label' + type: string + description: 'Label used for the download links. Defaults to Download.' + file: + title: File + type: object + description: 'File presented for download.' + properties: + title: + type: string + url: + type: string + size: + type: [string, integer] + extension: + type: string + language_code: + type: string + meta: + type: string + translations: + title: Translations + type: array + description: 'List of translations of the file.' + items: + type: object + properties: + title: + type: string + url: + type: string + size: + type: [string, integer] + extension: + type: string + language_code: + type: string + meta: + type: string + icon_path: + title: 'Icons path' + type: string + description: 'Path to the SVG icon file. Defaults to the BCL icon path.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/file/file.story.twig b/components/file/file.story.twig new file mode 100644 index 000000000..9f6e7e31f --- /dev/null +++ b/components/file/file.story.twig @@ -0,0 +1,49 @@ +{% set file = { + title: 'Example document', + url: 'https://example.com/document.pdf', + size: 4444, + extension: 'pdf', + language_code: 'en', +} %} + +
+
+

Default

+ {% include 'oe_bootstrap_theme:file' with { + title: 'Download the document', + file: file, + } only %} +
+ +
+

With translations

+ {% include 'oe_bootstrap_theme:file' with { + title: 'Download the document', + file: file, + translations: [ + { + title: 'Exemple de document', + url: 'https://example.com/document-fr.pdf', + size: 4321, + extension: 'pdf', + language_code: 'fr', + }, + { + title: 'Пример документ', + url: 'https://example.com/document-bg.docx', + size: 40391, + extension: 'docx', + language_code: 'bg', + }, + ], + } only %} +
+ +
+

Custom link label, without title

+ {% include 'oe_bootstrap_theme:file' with { + link_label: 'Get the file', + file: file, + } only %} +
+
diff --git a/components/file/file.twig b/components/file/file.twig new file mode 100644 index 000000000..2baa4d9f4 --- /dev/null +++ b/components/file/file.twig @@ -0,0 +1,81 @@ +{# +/** + * @file + * File component. + */ +#} +{%- set _icon_path = icon_path|default(bcl_icon_path()) -%} +{%- set _link_label = link_label|default('Download'|t) -%} + +{%- set _translations = {} -%} +{%- if translations is not empty and translations is iterable -%} + {%- set _items = [] -%} + {%- for translation in translations -%} + {%- set _translation_meta = translation.meta|default('') -%} + {%- if _translation_meta is empty -%} + {%- set _translation_meta = '(' ~ translation.extension|upper ~ ')' -%} + {%- if translation.size -%} + {%- set _translation_meta = '(' ~ translation.size|format_size ~ ' - ' ~ translation.extension|upper ~ ')' -%} + {%- endif -%} + {%- endif -%} + {%- set _items = _items|merge([{ + title: translation.title, + language: translation.language_code is not empty ? translation.language_code|to_native_language : '', + meta: _translation_meta, + download: { + label: _link_label, + path: translation.url, + }, + }]) -%} + {%- endfor -%} + {%- set _translations = { + label: { + path: _icon_path, + label: 'Other languages (@count)'|t({'@count': _items|length}), + icon_position: 'after', + icon: { + name: 'caret-down-fill', + size: '2xs', + path: _icon_path, + }, + }, + items: _items, + } -%} +{%- endif -%} + +{%- if file is not empty -%} + {%- set _meta = file.meta|default('') -%} + {%- if _meta is empty -%} + {%- set _meta = '(' ~ file.extension|upper ~ ')' -%} + {%- if file.size -%} + {%- set _meta = '(' ~ file.size|format_size ~ ' - ' ~ file.extension|upper ~ ')' -%} + {%- endif -%} + {%- endif -%} +
+ {%- include '@oe-bcl/file' with { + title: title, + title_tag: title_tag|default('h2'), + title_attributes: title_attributes, + item_title: file.title, + language: file.language_code is not empty ? file.language_code|to_native_language : '', + meta: _meta, + icon: { + path: _icon_path, + name: file.extension|to_file_icon, + size: '2xl', + }, + download: { + label: _link_label, + path: file.url, + icon: { + path: _icon_path, + name: 'download', + size: 'fluid', + }, + }, + translation: _translations, + icon_path: _icon_path, + attributes: attributes, + } only -%} +
+{%- endif -%} diff --git a/components/gallery/gallery.component.yml b/components/gallery/gallery.component.yml new file mode 100644 index 000000000..f3a79c282 --- /dev/null +++ b/components/gallery/gallery.component.yml @@ -0,0 +1,203 @@ +name: Gallery +description: 'A collection of images and videos displayed as thumbnails and in a modal carousel.' +props: + type: object + properties: + gallery_id: + title: 'Gallery ID' + type: string + description: 'Unique ID used to connect the gallery, modal and carousel.' + title: + title: Title + type: [string, array, object] + description: 'Title of the gallery.' + title_tag: + title: 'Title tag' + type: string + description: 'The tag to use for the title. Defaults to h2.' + title_link: + title: 'Title link' + type: object + description: 'Optional link applied to the gallery title.' + properties: + label: + type: [string, array, object] + path: + type: [string, object] + variant: + type: string + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + 'meta:enum': + primary: Primary + secondary: Secondary + success: Success + danger: Danger + warning: Warning + info: Info + light: Light + dark: Dark + id: + type: string + disabled: + type: boolean + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_display: + type: string + enum: + - flex + - inline-flex + remove_icon_spacers: + type: boolean + attributes: + type: object + title_attributes: + title: 'Title attributes' + type: Drupal\Core\Template\Attribute + counter: + title: Counter + type: string + description: 'Counter label. The %d placeholder is replaced by the number of items.' + collapse_button_label: + title: 'Collapsed label' + type: string + description: 'Label used to reveal hidden thumbnails. Defaults to View the full gallery (%d).' + expand_button_label: + title: 'Expanded label' + type: string + description: 'Label used to hide expanded thumbnails. Defaults to View less.' + previous_label: + title: 'Previous label' + type: string + description: 'Accessible label for the previous carousel control. Defaults to Previous.' + next_label: + title: 'Next label' + type: string + description: 'Accessible label for the next carousel control. Defaults to Next.' + max_visible_thumbnails: + title: 'Maximum visible thumbnails' + type: integer + description: 'Number of thumbnails visible before the gallery is collapsed. Defaults to 5.' + items: + title: Items + type: array + description: 'Images and playable media displayed in the gallery.' + items: + type: object + properties: + thumbnail: + title: Thumbnail + type: [string, array, object] + media: + title: Media + type: [string, array, object] + caption_title: + title: 'Caption title' + type: [string, array, object] + caption: + title: Caption + type: [string, array, object] + caption_classes: + title: 'Caption classes' + type: string + copyright: + title: Copyright + type: [string, array, object] + copyright_label: + title: 'Copyright label' + type: string + interval: + title: Interval + type: integer + link: + title: Link + type: object + properties: + label: + type: [string, array, object] + path: + type: [string, object] + variant: + type: string + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + 'meta:enum': + primary: Primary + secondary: Secondary + success: Success + danger: Danger + warning: Warning + info: Info + light: Light + dark: Dark + id: + type: string + disabled: + type: boolean + standalone: + type: boolean + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_display: + type: string + enum: + - flex + - inline-flex + remove_icon_spacers: + type: boolean + attributes: + type: object + icon_path: + title: 'Icons path' + type: string + description: 'Path to the SVG icon file. Defaults to the BCL icon path.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/gallery/gallery.story.twig b/components/gallery/gallery.story.twig new file mode 100644 index 000000000..79ebeaf1c --- /dev/null +++ b/components/gallery/gallery.story.twig @@ -0,0 +1,108 @@ +{% set gallery_items = [ + { + media: { + '#markup': 'Modern public building' + }, + thumbnail: { + '#markup': 'Modern public building thumbnail' + }, + caption_title: 'Public architecture', + caption: 'A contemporary public building designed around an open civic space.', + copyright: '© 2026 Example Organisation' + }, + { + media: { + '#markup': '', + '#allowed_tags': ['video', 'source'] + }, + thumbnail: { + '#markup': 'Video thumbnail' + }, + caption_title: 'Programme overview', + caption: 'A short introduction to the programme.', + copyright: '© 2026 Example Organisation' + }, + { + media: { + '#markup': 'People in a public square' + }, + thumbnail: { + '#markup': 'People in a public square thumbnail' + }, + caption_title: 'Connected communities', + caption: 'People meeting in a shared urban space.', + link: { + label: 'Read the story', + path: 'https://example.com/community', + icon_position: 'after', + icon: { + name: 'chevron-right' + } + } + }, + { + media: { + '#markup': '', + '#allowed_tags': ['iframe'] + }, + thumbnail: { + '#markup': 'Electric vehicle testing video thumbnail' + }, + caption_title: 'Safer electric vehicles', + caption: 'Harmonised test procedures for battery technology.' + }, + { + media: { + '#markup': 'Coastal landscape' + }, + thumbnail: { + '#markup': 'Coastal landscape thumbnail' + }, + caption_title: 'Coastal regions', + caption: 'Communities adapting to environmental change.' + }, + { + media: { + '#markup': 'Solar panels in a field' + }, + thumbnail: { + '#markup': 'Solar panels thumbnail' + }, + caption_title: 'Clean energy', + caption: 'Renewable energy infrastructure supporting the green transition.' + } +] %} + +
+
+

Compact gallery

+ {% include 'oe_bootstrap_theme:gallery' with { + title: 'Featured media', + items: gallery_items|slice(0, 4), + } only %} +
+ +
+

Mixed media gallery

+ {% include 'oe_bootstrap_theme:gallery' with { + gallery_id: 'gallery-story-mixed-media', + title: 'Projects across Europe', + title_tag: 'h4', + title_link: { + path: 'https://example.com/projects', + attributes: { + class: ['custom-gallery-title-link'] + } + }, + title_attributes: create_attribute().addClass('custom-gallery-title'), + counter: '%d media items', + collapse_button_label: 'Show all %d items', + expand_button_label: 'Show fewer items', + previous_label: 'Previous media', + next_label: 'Next media', + max_visible_thumbnails: 3, + items: gallery_items, + attributes: create_attribute().addClass('custom-gallery'), + } only %} +
+
diff --git a/components/gallery/gallery.twig b/components/gallery/gallery.twig new file mode 100644 index 000000000..c02a1ca0e --- /dev/null +++ b/components/gallery/gallery.twig @@ -0,0 +1,69 @@ +{# +/** + * @file + * Gallery component. + */ +#} +{% extends '@oe-bcl/gallery' %} + +{% set id = gallery_id|default('gallery'|clean_unique_id) %} +{% set title_tag = title_tag|default('h2') %} +{% set toggle_collapsed = collapse_button_label|default('View the full gallery (%d)'|t) %} +{% set toggle_expanded = expand_button_label|default('View less'|t) %} +{% set next_label = next_label|default('Next'|t) %} +{% set prev_label = previous_label|default('Previous'|t) %} +{% set max_visible_thumbnails = max_visible_thumbnails|default(5) %} +{% set icon_path = icon_path|default(bcl_icon_path()) %} + +{% set _title_link = title_link|default({}) %} +{% if _title_link is not empty %} + {% set _title_link = _title_link|merge({ + attributes: create_attribute(_title_link.attributes|default({})), + }) %} + {% if _title_link.icon is defined and _title_link.icon is not empty %} + {% set _title_link_icon = _title_link.icon|merge({ + path: _title_link.icon.path|default(icon_path), + attributes: create_attribute(_title_link.icon.attributes|default({})), + }) %} + {% set _title_link = _title_link|merge({icon: _title_link_icon}) %} + {% endif %} +{% endif %} +{% set title_link = _title_link %} + +{% set _gallery_items = [] %} +{% for item in items|default([]) %} + {% set _item = item %} + {% if _item.link is defined and _item.link is not empty %} + {% set _item_link = _item.link|merge({ + attributes: create_attribute(_item.link.attributes|default({})), + }) %} + {% if _item_link.icon is defined and _item_link.icon is not empty %} + {% set _item_link_icon = _item_link.icon|merge({ + path: _item_link.icon.path|default(icon_path), + attributes: create_attribute(_item_link.icon.attributes|default({})), + }) %} + {% set _item_link = _item_link|merge({icon: _item_link_icon}) %} + {% endif %} + {% set _item = _item|merge({link: _item_link}) %} + {% endif %} + {% set _gallery_items = _gallery_items|merge([_item]) %} +{% endfor %} +{% set items = bcl_gallery_items(_gallery_items) %} + +{% block carousel %} + {# Captions are not rendered for playable media in the carousel. #} + {% include '@oe-bcl/carousel' with { + id: _carousel_id, + with_controls: true, + with_indicators: false, + prev_label: _prev_label, + next_label: _next_label, + autoplay: false, + items: items|map(_item => _item|merge({ + caption: _item.is_playable ? '' : _item.caption, + caption_title: _item.is_playable ? '' : _item.caption_title, + link: _item.is_playable ? {} : _item.link, + copyright_label: _item.copyright_label|default(_item.is_playable ? 'Copyright:'|t : 'Image credit:'|t), + })), + } only %} +{% endblock %} diff --git a/components/icon/icon.component.yml b/components/icon/icon.component.yml new file mode 100644 index 000000000..9e315b453 --- /dev/null +++ b/components/icon/icon.component.yml @@ -0,0 +1,54 @@ +name: Icon +description: 'Displays an icon from an SVG sprite.' +props: + type: object + properties: + name: + title: Name + type: string + description: 'Name of the icon in the SVG sprite.' + size: + title: Size + type: string + description: 'Size of the icon. Defaults to s.' + enum: + - 2xs + - xs + - s + - m + - l + - xl + - 2xl + - fluid + 'meta:enum': + 2xs: '2XS' + xs: XS + s: 'S (default)' + m: M + l: L + xl: XL + 2xl: '2XL' + fluid: Fluid + transformation: + title: Transformation + type: string + description: 'Optional rotation or flip transformation.' + enum: + - rotate-90 + - rotate-180 + - rotate-270 + - flip-horizontal + - flip-vertical + 'meta:enum': + rotate-90: 'Rotate 90 degrees' + rotate-180: 'Rotate 180 degrees' + rotate-270: 'Rotate 270 degrees' + flip-horizontal: 'Flip horizontally' + flip-vertical: 'Flip vertically' + icon_path: + title: 'Icon path' + type: string + description: 'Path to the SVG sprite. Defaults to the BCL icon path.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/icon/icon.story.twig b/components/icon/icon.story.twig new file mode 100644 index 000000000..df47a10a1 --- /dev/null +++ b/components/icon/icon.story.twig @@ -0,0 +1,73 @@ +{% set sizes = ['2xs', 'xs', 's', 'm', 'l', 'xl', '2xl', 'fluid'] %} +{% set transformations = [ + { value: 'rotate-90', label: 'Rotate 90 degrees' }, + { value: 'rotate-180', label: 'Rotate 180 degrees' }, + { value: 'rotate-270', label: 'Rotate 270 degrees' }, + { value: 'flip-horizontal', label: 'Flip horizontally' }, + { value: 'flip-vertical', label: 'Flip vertically' }, +] %} + + + +
+
+

Sizes

+
+ {% for size in sizes %} +
+
+ {% include 'oe_bootstrap_theme:icon' with { + name: 'eye', + size: size, + attributes: create_attribute().addClass('text-primary').setAttribute('aria-hidden', 'true'), + } only %} +
+
{{ size }}
+
+ {% endfor %} +
+
+ +
+

Transformations

+
+ {% for transformation in transformations %} +
+
+ {% include 'oe_bootstrap_theme:icon' with { + name: 'arrow-right', + size: 'xl', + transformation: transformation.value, + attributes: create_attribute().addClass('text-secondary').setAttribute('aria-hidden', 'true'), + } only %} +
+
{{ transformation.label }}
+
+ {% endfor %} +
+
+ +
+

Custom attributes and path

+ {% include 'oe_bootstrap_theme:icon' with { + name: 'award', + size: '2xl', + icon_path: bcl_icon_path(), + attributes: create_attribute({ + id: 'custom-icon', + class: ['text-success', 'custom-icon-class'], + role: 'img', + 'aria-label': 'Award', + }), + } only %} +
+
diff --git a/components/icon/icon.twig b/components/icon/icon.twig new file mode 100644 index 000000000..8da86f5db --- /dev/null +++ b/components/icon/icon.twig @@ -0,0 +1,13 @@ +{# +/** + * @file + * Icon component. + */ +#} +{% include '@oe-bcl/icon' with { + name: name, + path: icon_path|default(bcl_icon_path()), + size: size|default('s'), + transformation: transformation, + attributes: attributes, +} only %} diff --git a/components/inpage_navigation/inpage_navigation.component.yml b/components/inpage_navigation/inpage_navigation.component.yml new file mode 100644 index 000000000..553785dbf --- /dev/null +++ b/components/inpage_navigation/inpage_navigation.component.yml @@ -0,0 +1,177 @@ +name: 'Inpage navigation' +description: 'Provides a sticky table of contents alongside page content and updates the active item while scrolling.' +props: + type: object + properties: + title: + title: Title + type: [string, array, object] + description: 'Title of the navigation.' + title_tag: + title: 'Title tag' + type: string + description: 'HTML tag used for the title. Defaults to h2.' + title_link: + title: 'Title link' + type: object + description: 'Optional link applied to the title.' + properties: + label: + type: [string, array, object] + path: + type: [string, object] + variant: + type: string + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + id: + type: string + disabled: + type: boolean + icon: + type: [string, object] + properties: + name: + type: string + path: + type: string + size: + type: string + enum: + - 2xs + - xs + - s + - m + - l + - xl + - 2xl + - fluid + transformation: + type: string + enum: + - rotate-90 + - rotate-180 + - rotate-270 + - flip-horizontal + - flip-vertical + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_display: + type: string + enum: + - flex + - inline-flex + remove_icon_spacers: + type: boolean + attributes: + type: object + title_attributes: + title: 'Title attributes' + type: Drupal\Core\Template\Attribute + links: + title: Links + type: array + description: 'Links to headings in the page content.' + items: + type: object + properties: + label: + type: [string, array, object] + path: + type: [string, object] + variant: + type: string + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + id: + type: string + disabled: + type: boolean + standalone: + type: boolean + active: + type: boolean + icon: + type: [string, object] + properties: + name: + type: string + path: + type: string + size: + type: string + enum: + - 2xs + - xs + - s + - m + - l + - xl + - 2xl + - fluid + transformation: + type: string + enum: + - rotate-90 + - rotate-180 + - rotate-270 + - flip-horizontal + - flip-vertical + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_display: + type: string + enum: + - flex + - inline-flex + remove_icon_spacers: + type: boolean + attributes: + type: object + content: + title: Content + type: [string, array, object] + description: 'Page content displayed alongside the navigation.' + id: + title: ID + type: string + description: 'Navigation ID. A unique ID is generated when omitted.' + dropdown_id: + title: 'Dropdown ID' + type: string + description: 'Mobile dropdown trigger ID. Defaults to the navigation ID followed by -dropdown.' + dynamic_active: + title: 'Dynamic active' + type: boolean + description: 'Automatically controls the active item on mobile. Defaults to false.' + icon_path: + title: 'Icons path' + type: string + description: 'Path to the SVG icon file. Defaults to the BCL icon path.' + attributes: + title: 'Navigation attributes' + type: Drupal\Core\Template\Attribute diff --git a/components/inpage_navigation/inpage_navigation.story.twig b/components/inpage_navigation/inpage_navigation.story.twig new file mode 100644 index 000000000..ce402dc61 --- /dev/null +++ b/components/inpage_navigation/inpage_navigation.story.twig @@ -0,0 +1,23 @@ +{% set default_links = [ + { label: 'Overview', path: '#overview' }, + { label: 'Eligibility', path: '#eligibility' }, + { label: 'Apply', path: '#how-to-apply' }, + { label: 'Support', path: '#support' }, +] %} +{% set default_content %} +

Overview

+

This funding programme supports projects that strengthen cooperation and create measurable benefits across Europe.

+

Eligibility

+

Public authorities, non-profit organisations, and research institutions established in eligible countries can participate.

+

How to apply

+

Prepare the required documents, complete the application form, and submit the proposal before the published deadline.

+

Support

+

Contact the programme helpdesk for guidance about eligibility, documentation, or the submission process.

+{% endset %} + +{% include 'oe_bootstrap_theme:inpage_navigation' with { + title: 'Contents', + links: default_links, + content: default_content, + dynamic_active: true, +} only %} diff --git a/components/inpage_navigation/inpage_navigation.twig b/components/inpage_navigation/inpage_navigation.twig new file mode 100644 index 000000000..3c99fbd1d --- /dev/null +++ b/components/inpage_navigation/inpage_navigation.twig @@ -0,0 +1,67 @@ +{# +/** + * @file + * Inpage navigation component. + */ +#} +{% set _icon_path = icon_path|default(bcl_icon_path()) %} +{% set _attributes = attributes|default(create_attribute()) %} +{% set _id = id|default(_attributes.id|default('bcl-inpage-navigation'|clean_unique_id)) %} +{% set _dropdown_id = dropdown_id|default(_id ~ '-dropdown') %} +{% set _title_link = title_link|default({}) %} + +{% if _title_link is not empty %} + {% set _title_link = _title_link|merge({ + attributes: create_attribute(_title_link.attributes|default({})), + }) %} + {% if _title_link.icon is defined and _title_link.icon is not empty %} + {% set _title_icon = _title_link.icon is iterable ? _title_link.icon : { name: _title_link.icon } %} + {% set _title_link = _title_link|merge({ + icon: _title_icon|merge({ + path: _title_icon.path|default(_icon_path), + attributes: create_attribute(_title_icon.attributes|default({})), + }), + }) %} + {% endif %} +{% endif %} + +{% set _links = [] %} +{% for link in links|default([]) %} + {% set _link = link|merge({ + attributes: create_attribute(link.attributes|default({})), + }) %} + {% if _link.icon is defined and _link.icon is not empty %} + {% set _link_icon = _link.icon is iterable ? _link.icon : { name: _link.icon } %} + {% set _link = _link|merge({ + icon: _link_icon|merge({ + path: _link_icon.path|default(_icon_path), + attributes: create_attribute(_link_icon.attributes|default({})), + }), + }) %} + {% endif %} + {% set _links = _links|merge([_link]) %} +{% endfor %} + +{% if _links is not empty %} + {{ attach_library('oe_bootstrap_theme/inpage_navigation') }} +{% endif %} + +
+
+ {% include '@oe-bcl/inpage-navigation' with { + title: title, + title_tag: title_tag, + title_link: _title_link, + title_attributes: title_attributes, + links: _links, + id: _id, + dropdown_id: _dropdown_id, + dynamic_active: dynamic_active, + icon_path: _icon_path, + attributes: _attributes, + } only %} +
+
+ {{- content -}} +
+
diff --git a/components/link/link.component.yml b/components/link/link.component.yml new file mode 100644 index 000000000..f58936af6 --- /dev/null +++ b/components/link/link.component.yml @@ -0,0 +1,106 @@ +name: Link +description: 'A link with optional color, icon, disabled and standalone styles.' +props: + type: object + properties: + label: + title: Label + type: [string, array, object] + description: 'Link label text or render array.' + path: + title: Path + type: [string, object] + description: 'Link URL string or URL object. Defaults to #.' + variant: + title: Variant + type: string + description: 'Link color variant. Defaults to the standard link style.' + enum: + - '' + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + 'meta:enum': + '': Default + primary: Primary + secondary: Secondary + success: Success + danger: Danger + warning: Warning + info: Info + light: Light + dark: Dark + id: + title: ID + type: string + description: 'Link ID attribute.' + disabled: + title: Disabled + type: boolean + description: 'Disables keyboard and pointer interaction. Defaults to false.' + standalone: + title: Standalone + type: boolean + description: 'Applies standalone link styling. Defaults to false.' + icon: + title: Icon + type: [string, object] + description: 'Icon name as a string or an icon object.' + properties: + name: + type: string + path: + type: string + size: + type: string + enum: + - 2xs + - xs + - s + - m + - l + - xl + - 2xl + - fluid + transformation: + type: string + enum: + - rotate-90 + - rotate-180 + - rotate-270 + - flip-horizontal + - flip-vertical + attributes: + type: object + icon_position: + title: 'Icon position' + type: string + description: 'Position of the icon relative to the label. Defaults to after.' + enum: + - before + - after + 'meta:enum': + before: Before + after: 'After (default)' + icon_display: + title: 'Icon display' + type: string + description: 'Uses flexbox gap spacing between the icon and label.' + enum: + - flex + - inline-flex + 'meta:enum': + flex: Flex + inline-flex: 'Inline flex' + remove_icon_spacers: + title: 'Remove icon spacers' + type: boolean + description: 'Removes spacing between the icon and label. Defaults to false.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/link/link.story.twig b/components/link/link.story.twig new file mode 100644 index 000000000..4a108bcfb --- /dev/null +++ b/components/link/link.story.twig @@ -0,0 +1,115 @@ +{% set variants = [ + 'primary', + 'secondary', + 'success', + 'danger', + 'warning', + 'info', + 'light', + 'dark', +] %} + +
+
+

Variants

+
+ {% include 'oe_bootstrap_theme:link' with { + label: 'Default', + path: '/example', + } only %} + + {% for variant in variants %} + + {% include 'oe_bootstrap_theme:link' with { + label: variant|capitalize, + path: '/example', + variant: variant, + } only %} + + {% endfor %} +
+
+ +
+

Icons

+
+ {% include 'oe_bootstrap_theme:link' with { + label: 'Icon before', + path: '/example', + icon: 'arrow-left', + icon_position: 'before', + } only %} + + {% include 'oe_bootstrap_theme:link' with { + label: 'Icon after', + path: '/example', + icon: 'arrow-right', + icon_position: 'after', + } only %} + + {% include 'oe_bootstrap_theme:link' with { + label: 'Inline flex', + path: '/example', + icon: 'box-arrow-up-right', + icon_display: 'inline-flex', + } only %} + + {% include 'oe_bootstrap_theme:link' with { + label: 'Flex', + path: '/example', + icon: 'download', + icon_position: 'before', + icon_display: 'flex', + } only %} + + {% include 'oe_bootstrap_theme:link' with { + label: 'No icon spacer', + path: '/example', + icon: 'arrow-right', + remove_icon_spacers: true, + } only %} + + {% include 'oe_bootstrap_theme:link' with { + path: '/search', + icon: 'search', + attributes: create_attribute().setAttribute('aria-label', 'Search'), + } only %} + + {% include 'oe_bootstrap_theme:link' with { + label: 'Transformed custom icon', + path: '/example', + icon: { + name: 'arrow-right', + path: bcl_icon_path(), + size: 'xl', + transformation: 'rotate-90', + attributes: { + class: ['text-success'], + 'aria-hidden': 'true', + }, + }, + } only %} +
+
+ +
+

States and attributes

+
+ {% include 'oe_bootstrap_theme:link' with { + label: 'Disabled standalone link', + path: '/example', + disabled: true, + standalone: true, + } only %} + + {% include 'oe_bootstrap_theme:link' with { + label: [{ '#markup': 'Label with markup' }], + path: '/example', + id: 'custom-link', + attributes: create_attribute() + .addClass('custom-link-class') + .setAttribute('data-link-attribute', 'custom-value'), + } only %} +
+
+
diff --git a/components/link/link.twig b/components/link/link.twig new file mode 100644 index 000000000..afbd9ca50 --- /dev/null +++ b/components/link/link.twig @@ -0,0 +1,30 @@ +{# +/** + * @file + * Link component. + */ +#} +{% set _icon = {} %} +{% if icon is defined and icon %} + {% set _icon = icon is iterable ? icon : { name: icon } %} + {% if _icon.path is not defined or _icon.path is empty %} + {% set _icon = _icon|merge({ path: bcl_icon_path() }) %} + {% endif %} + {% set _icon = _icon|merge({ + attributes: create_attribute(_icon.attributes|default({})), + }) %} +{% endif %} + +{% include '@oe-bcl/link' with { + label: label, + path: path, + variant: variant, + id: id, + disabled: disabled, + standalone: standalone, + icon: _icon, + icon_position: icon_position, + icon_display: icon_display, + remove_icon_spacers: remove_icon_spacers, + attributes: attributes, +} only %} diff --git a/components/links_block/links_block.component.yml b/components/links_block/links_block.component.yml new file mode 100644 index 000000000..c4ee357d5 --- /dev/null +++ b/components/links_block/links_block.component.yml @@ -0,0 +1,179 @@ +name: 'Links block' +description: 'Displays a list of links with an optional title.' +props: + type: object + properties: + title: + title: Title + type: [string, array, object] + description: 'Title of the links block.' + title_tag: + title: 'Title tag' + type: string + description: 'HTML tag used for the title. Defaults to h2.' + title_link: + title: 'Title link' + type: object + description: 'Optional link applied to the title.' + properties: + label: + type: [string, array, object] + path: + type: [string, object] + variant: + type: string + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + id: + type: string + disabled: + type: boolean + icon: + type: [string, object] + properties: + name: + type: string + path: + type: string + size: + type: string + enum: + - 2xs + - xs + - s + - m + - l + - xl + - 2xl + - fluid + transformation: + type: string + enum: + - rotate-90 + - rotate-180 + - rotate-270 + - flip-horizontal + - flip-vertical + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_display: + type: string + enum: + - flex + - inline-flex + remove_icon_spacers: + type: boolean + attributes: + type: object + title_attributes: + title: 'Title attributes' + type: Drupal\Core\Template\Attribute + background: + title: Background + type: string + description: 'Background style of the block. Defaults to gray.' + default: gray + enum: + - gray + - transparent + 'meta:enum': + gray: 'Gray (default)' + transparent: Transparent + orientation: + title: Orientation + type: string + description: 'Direction in which links are displayed. Defaults to vertical.' + default: vertical + enum: + - vertical + - horizontal + 'meta:enum': + vertical: 'Vertical (default)' + horizontal: Horizontal + links: + title: Links + type: array + description: 'Links displayed in the block.' + items: + type: object + properties: + label: + type: [string, array, object] + path: + type: [string, object] + variant: + type: string + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + id: + type: string + disabled: + type: boolean + icon: + type: [string, object] + properties: + name: + type: string + path: + type: string + size: + type: string + enum: + - 2xs + - xs + - s + - m + - l + - xl + - 2xl + - fluid + transformation: + type: string + enum: + - rotate-90 + - rotate-180 + - rotate-270 + - flip-horizontal + - flip-vertical + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_display: + type: string + enum: + - flex + - inline-flex + remove_icon_spacers: + type: boolean + attributes: + type: object + icon_path: + title: 'Icons path' + type: string + description: 'Path to the SVG icon file. Defaults to the BCL icon path.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/links_block/links_block.story.twig b/components/links_block/links_block.story.twig new file mode 100644 index 000000000..2b132f1e7 --- /dev/null +++ b/components/links_block/links_block.story.twig @@ -0,0 +1,38 @@ +{% set resource_links = [ + { + label: 'Funding opportunities', + path: '/example', + icon: 'arrow-right', + icon_position: 'before', + }, + { + label: 'Calls for proposals', + path: '/example', + icon: 'arrow-right', + icon_position: 'before', + }, + { + label: 'Programme guidance', + path: '/example', + icon: 'arrow-right', + icon_position: 'before', + }, +] %} + +
+ {% include 'oe_bootstrap_theme:links_block' with { + title: 'Useful resources', + links: resource_links, + } only %} + + {% include 'oe_bootstrap_theme:links_block' with { + title: 'Related topics', + background: 'transparent', + orientation: 'horizontal', + links: [ + { label: 'Digital strategy', path: '/example' }, + { label: 'Research and innovation', path: '/example' }, + { label: 'Regional development', path: '/example' }, + ], + } only %} +
diff --git a/components/links_block/links_block.twig b/components/links_block/links_block.twig new file mode 100644 index 000000000..b22c84189 --- /dev/null +++ b/components/links_block/links_block.twig @@ -0,0 +1,54 @@ +{# +/** + * @file + * Links block component. + */ +#} +{% set _icon_path = icon_path|default(bcl_icon_path()) %} +{% set _title_link = title_link|default({}) %} + +{% if _title_link is not empty %} + {% set _title_link = _title_link|merge({ + attributes: create_attribute(_title_link.attributes|default({})), + }) %} + {% if _title_link.icon is defined and _title_link.icon is not empty %} + {% set _title_icon = _title_link.icon is iterable ? _title_link.icon : { name: _title_link.icon } %} + {% set _title_link = _title_link|merge({ + icon: _title_icon|merge({ + path: _title_icon.path|default(_icon_path), + attributes: create_attribute(_title_icon.attributes|default({})), + }), + }) %} + {% endif %} +{% endif %} + +{% set _links = [] %} +{% for link in links|default([]) %} + {% set _link = link|merge({ + attributes: create_attribute(link.attributes|default({})), + }) %} + {% if _link.icon is defined and _link.icon is not empty %} + {% set _link_icon = _link.icon is iterable ? _link.icon : { name: _link.icon } %} + {% set _link = _link|merge({ + icon: _link_icon|merge({ + path: _link_icon.path|default(_icon_path), + attributes: create_attribute(_link_icon.attributes|default({})), + }), + }) %} + {% if _link.icon_display is not defined %} + {% set _link = _link|merge({ icon_display: 'flex' }) %} + {% endif %} + {% endif %} + {% set _links = _links|merge([_link]) %} +{% endfor %} + +{% include '@oe-bcl/links-block' with { + title: title, + title_tag: title_tag, + title_link: _title_link, + title_attributes: title_attributes, + background: background, + variant: orientation, + links: _links, + attributes: attributes, +} only %} diff --git a/components/list_group/list_group.component.yml b/components/list_group/list_group.component.yml new file mode 100644 index 000000000..c622407f2 --- /dev/null +++ b/components/list_group/list_group.component.yml @@ -0,0 +1,254 @@ +name: 'List group' +description: 'Displays a flexible series of content as unordered, ordered, or actionable items.' +props: + type: object + properties: + flush: + title: Flush + type: boolean + description: 'Removes outer borders and rounded corners. Defaults to false.' + horizontal: + title: Horizontal + type: string + description: 'Displays items horizontally, optionally from a responsive breakpoint.' + enum: + - horizontal + - horizontal-sm + - horizontal-md + - horizontal-lg + - horizontal-xl + - horizontal-xxl + 'meta:enum': + horizontal: 'All viewports' + horizontal-sm: 'Small and larger' + horizontal-md: 'Medium and larger' + horizontal-lg: 'Large and larger' + horizontal-xl: 'Extra large and larger' + horizontal-xxl: 'Extra extra large and larger' + id: + title: ID + type: string + description: 'List group ID attribute.' + type: + title: Type + type: string + description: 'Type of list group. Defaults to unordered.' + enum: + - unordered + - ordered + - actionable + 'meta:enum': + unordered: 'Unordered (default)' + ordered: Ordered + actionable: Actionable + items: + title: Items + type: array + description: 'List content, form inputs, links, or buttons.' + items: + type: object + properties: + html: + type: [string, array, object] + variant: + type: string + enum: + - '' + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + extra_classes: + type: string + badge: + type: object + properties: + label: + type: [string, array, object] + background: + type: string + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + url: + type: string + title: + type: string + dismissible: + type: boolean + assistive_text: + type: string + rounded_pill: + type: boolean + outline: + type: boolean + icon_path: + type: string + attributes: + type: object + input: + type: boolean + input_type: + type: string + label: + type: [string, array, object] + required: + type: boolean + checked: + type: boolean + hidden_label: + type: boolean + disabled: + type: boolean + readonly: + type: boolean + invalid: + type: boolean + valid: + type: boolean + invalid_feedback: + type: [string, array, object] + valid_feedback: + type: [string, array, object] + floating: + type: boolean + helper_text: + type: [string, array, object] + helper_text_id: + type: string + placeholder: + type: string + horizontal: + type: boolean + horizontal_class: + type: string + horizontal_label_class: + type: string + switch: + type: boolean + toggle: + type: boolean + toggle_variant: + type: string + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + remove_wrapper: + type: boolean + path: + type: [string, object] + button: + type: boolean + id: + type: string + type: + type: string + enum: + - button + - submit + - reset + text_nowrap: + type: boolean + assistive_text: + type: string + standalone: + type: boolean + icon: + type: [string, object] + properties: + name: + type: string + path: + type: string + size: + type: string + enum: + - 2xs + - xs + - s + - m + - l + - xl + - 2xl + - fluid + transformation: + type: string + enum: + - rotate-90 + - rotate-180 + - rotate-270 + - flip-horizontal + - flip-vertical + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_display: + type: string + enum: + - flex + - inline-flex + icon_spacing: + type: string + enum: + - sm + - md + - lg + remove_icon_spacers: + type: boolean + spinner: + type: object + properties: + type: + type: string + enum: + - border + - grow + variant: + type: string + enum: + - '' + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + size: + type: string + enum: + - '' + - sm + - lg + assistive_text: + type: string + attributes: + type: object + show_spinner: + type: boolean + attributes: + type: object + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/list_group/list_group.story.twig b/components/list_group/list_group.story.twig new file mode 100644 index 000000000..859cb8a85 --- /dev/null +++ b/components/list_group/list_group.story.twig @@ -0,0 +1,158 @@ +{% set variants = [ + 'primary', + 'secondary', + 'success', + 'danger', + 'warning', + 'info', + 'light', + 'dark', +] %} +{% set variant_items = [] %} +{% for variant in variants %} + {% set variant_items = variant_items|merge([{ + html: variant|capitalize ~ ' item', + variant: variant, + }]) %} +{% endfor %} +{% set variant_items = variant_items|merge([ + { + html: [{ '#markup': 'Item with rendered markup' }], + extra_classes: 'fw-medium', + }, + { + html: 'Item with a badge', + badge: { + label: '10', + background: 'success', + rounded_pill: true, + attributes: { class: ['float-end'] }, + }, + }, +]) %} + +
+
+

Unordered items and variants

+ {% include 'oe_bootstrap_theme:list_group' with { + items: variant_items, + } only %} +
+ +
+

Ordered and flush

+ {% include 'oe_bootstrap_theme:list_group' with { + type: 'ordered', + flush: true, + id: 'ordered-list-group', + attributes: create_attribute() + .addClass('custom-list-group') + .setAttribute('data-list-group', 'ordered'), + items: [ + { html: 'First step' }, + { html: 'Second step' }, + { html: 'Third step' }, + ], + } only %} +
+ +
+

Horizontal

+ {% include 'oe_bootstrap_theme:list_group' with { + horizontal: 'horizontal', + items: [ + { html: 'First item' }, + { html: 'Second item' }, + { html: 'Third item' }, + ], + } only %} + +

Horizontal from medium

+ {% include 'oe_bootstrap_theme:list_group' with { + horizontal: 'horizontal-md', + items: [ + { html: 'First item' }, + { html: 'Second item' }, + { html: 'Third item' }, + ], + } only %} +
+ +
+

Form inputs

+ {% include 'oe_bootstrap_theme:list_group' with { + items: [ + { + input: true, + input_type: 'checkbox', + id: 'list-group-checkbox', + label: 'Checked switch', + checked: true, + required: true, + switch: true, + attributes: { class: ['custom-checkbox'] }, + }, + { + input: true, + input_type: 'text', + id: 'list-group-text', + label: 'Validated text input', + placeholder: 'Enter a value', + valid: true, + valid_feedback: 'The value is valid.', + helper_text: 'Supporting text for this field.', + helper_text_id: 'list-group-text-help', + }, + { + input: true, + input_type: 'text', + id: 'list-group-invalid', + label: 'Invalid floating input', + placeholder: 'Enter a value', + floating: true, + invalid: true, + invalid_feedback: 'Enter a valid value.', + }, + ], + } only %} +
+ +
+

Actionable links and buttons

+ {% include 'oe_bootstrap_theme:list_group' with { + type: 'actionable', + items: [ + { + label: 'Link with icon', + path: '/example', + variant: 'primary', + icon: 'arrow-right', + icon_position: 'before', + attributes: { class: ['custom-action-link'] }, + }, + { + label: 'Disabled standalone link', + path: '/example', + disabled: true, + standalone: true, + }, + { + button: true, + label: 'Button with spinner', + type: 'button', + icon: { + name: 'check', + transformation: 'rotate-90', + attributes: { class: ['custom-button-icon'] }, + }, + show_spinner: true, + spinner: { + size: 'sm', + assistive_text: 'Loading', + }, + attributes: { class: ['custom-action-button'] }, + }, + ], + } only %} +
+
diff --git a/components/list_group/list_group.twig b/components/list_group/list_group.twig new file mode 100644 index 000000000..61d9940ee --- /dev/null +++ b/components/list_group/list_group.twig @@ -0,0 +1,51 @@ +{# +/** + * @file + * List group component. + */ +#} +{% set _icon_path = bcl_icon_path() %} +{% set _items = [] %} + +{% for item in items|default([]) %} + {% set _item = item|merge({ + attributes: create_attribute(item.attributes|default({})), + }) %} + + {% if _item.badge is defined and _item.badge is not empty %} + {% set _badge = _item.badge|merge({ + icon_path: _item.badge.icon_path|default(_icon_path), + attributes: create_attribute(_item.badge.attributes|default({})), + }) %} + {% set _item = _item|merge({ badge: _badge }) %} + {% endif %} + + {% if _item.icon is defined and _item.icon is not empty %} + {% set _icon = _item.icon is iterable ? _item.icon : { name: _item.icon } %} + {% set _item = _item|merge({ + icon: _icon|merge({ + path: _icon.path|default(_icon_path), + attributes: create_attribute(_icon.attributes|default({})), + }), + }) %} + {% endif %} + + {% if _item.spinner is defined and _item.spinner is not empty %} + {% set _item = _item|merge({ + spinner: _item.spinner|merge({ + attributes: create_attribute(_item.spinner.attributes|default({})), + }), + }) %} + {% endif %} + + {% set _items = _items|merge([_item]) %} +{% endfor %} + +{% include '@oe-bcl/list-group' with { + flush: flush, + horizontal: horizontal, + id: id, + type: type, + items: _items, + attributes: attributes, +} only %} diff --git a/components/listing/listing.component.yml b/components/listing/listing.component.yml new file mode 100644 index 000000000..3afea0fa0 --- /dev/null +++ b/components/listing/listing.component.yml @@ -0,0 +1,290 @@ +name: Listing +description: 'Displays cards in default or highlighted one-, two-, or three-column listings.' +props: + type: object + properties: + variant: + title: Variant + type: string + description: 'Visual style of the listing. Defaults to default.' + enum: + - default + - highlight + 'meta:enum': + default: 'Default (default)' + highlight: Highlight + columns: + title: Columns + type: integer + description: 'Number of columns. Defaults to 1.' + enum: + - 1 + - 2 + - 3 + 'meta:enum': + 1: 'One (default)' + 2: Two + 3: Three + title: + title: Title + type: [string, array, object] + description: 'Title of the listing.' + title_tag: + title: 'Title tag' + type: string + description: 'The tag to use for the title. Defaults to h2.' + title_link: + title: 'Title link' + type: object + description: 'Optional link applied to the listing title. The title prop is used as its label.' + properties: + label: + type: [string, array, object] + path: + type: [string, object] + variant: + type: string + id: + type: string + disabled: + type: boolean + standalone: + type: boolean + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_display: + type: string + enum: + - flex + - inline-flex + remove_icon_spacers: + type: boolean + attributes: + type: object + title_attributes: + title: 'Title attributes' + type: Drupal\Core\Template\Attribute + items: + title: Items + type: array + description: 'Cards displayed in the listing.' + items: + type: object + properties: + title: + type: [string, array, object] + title_tag: + type: string + title_link: + type: object + description: 'Optional link applied to the card title. The title prop is used as its label.' + properties: + path: + type: [string, object] + variant: + type: string + id: + type: string + disabled: + type: boolean + standalone: + type: boolean + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_display: + type: string + enum: + - flex + - inline-flex + remove_icon_spacers: + type: boolean + attributes: + type: object + url: + type: object + description: 'Drupal URL object optionally applied to the card title.' + subtitle: + type: [string, array, object] + text: + type: [string, array, object] + image: + type: object + properties: + src: + type: string + alt: + type: string + position: + type: string + enum: + - top + - bottom + - background + classes: + type: string + rounded: + type: string + image_copyright: + type: [string, array, object] + image_copyright_label: + type: string + description: 'Assistive label displayed before the image copyright. Defaults to "Image credit:".' + date: + type: object + properties: + day: + type: [string, integer] + month: + type: string + date_time: + type: string + year: + type: [string, integer] + end_day: + type: [string, integer] + end_month: + type: string + end_year: + type: [string, integer] + icon_path: + type: string + attributes: + type: object + content: + type: [string, array, object] + header: + type: [string, array, object] + footer: + type: [string, array, object] + badges: + type: array + items: + type: [string, object] + properties: + label: + type: [string, array, object] + background: + type: string + url: + type: string + title: + type: string + dismissible: + type: boolean + assistive_text: + type: string + rounded_pill: + type: boolean + outline: + type: boolean + icon_path: + type: string + attributes: + type: object + link: + title: Link + type: object + description: 'Optional call-to-action link displayed below the cards.' + properties: + label: + type: [string, array, object] + path: + type: [string, object] + variant: + type: string + id: + type: string + disabled: + type: boolean + standalone: + type: boolean + icon: + type: object + properties: + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_display: + type: string + enum: + - flex + - inline-flex + remove_icon_spacers: + type: boolean + attributes: + type: object + alignment: + title: Alignment + type: string + description: 'Vertical alignment of card media and content.' + enum: + - start + - end + - center + - baseline + - stretch + 'meta:enum': + start: Start + end: End + center: Center + baseline: Baseline + stretch: Stretch + image_size: + title: 'Image size' + type: string + description: 'Width of card media in horizontal listings.' + enum: + - small + - large + - extra-large + 'meta:enum': + small: Small + large: Large + extra-large: 'Extra large' + icon_path: + title: 'Icons path' + type: string + description: 'Path to the icons SVG file. Defaults to the BCL icon path.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/listing/listing.story.twig b/components/listing/listing.story.twig new file mode 100644 index 000000000..ad6c259b3 --- /dev/null +++ b/components/listing/listing.story.twig @@ -0,0 +1,261 @@ +{% set items = [ + { + title: 'Digital policy and legislation', + title_link: { + path: '/digital-policy', + }, + text: 'Current initiatives, legislation and practical guidance for Europe’s digital transformation.', + image: { + src: 'https://picsum.photos/seed/listing-policy/800/500', + alt: 'People working together in an office', + }, + image_copyright: '© European Union, 2026', + content: { + '#markup': 'Policy · 24 July 2026', + }, + badges: [ + 'Digital', + { + label: 'Policy', + background: 'success', + }, + ], + }, + { + title: 'Funding opportunities', + title_link: { + path: '/funding', + }, + text: 'Find open calls, programme information and resources for applicants and beneficiaries.', + image: { + src: 'https://picsum.photos/seed/listing-funding/800/500', + alt: 'Modern public building', + classes: 'mb-3', + }, + content: { + '#markup': 'Funding · 18 July 2026', + }, + }, + { + title: 'Upcoming consultation', + title_link: { + path: '/consultation', + }, + text: 'Contribute your views to an open consultation before the submission deadline.', + date: { + day: 12, + month: 'Sep', + year: 2026, + date_time: '2026-09-12', + }, + content: { + '#markup': 'Consultation · Open', + }, + }, +] %} + +{% set image_items = items|slice(0, 2)|merge([ + { + title: 'Research and innovation', + title_link: { + path: '/research', + }, + text: 'Explore European research programmes, scientific results and opportunities for collaboration.', + image: { + src: 'https://picsum.photos/seed/listing-research/800/500', + alt: 'Research facility interior', + classes: 'mb-3', + }, + content: { + '#markup': 'Research · 10 July 2026', + }, + }, +]) %} + +{% set date_items = items|slice(2, 1)|merge([ + { + title: 'Information session', + title_link: { + path: '/information-session', + }, + text: 'Join an online session covering the programme, eligibility requirements and application process.', + date: { + day: 4, + month: 'Oct', + year: 2026, + date_time: '2026-10-04', + }, + content: { + '#markup': 'Online event · Registration open', + }, + }, + { + title: 'Application deadline', + title_link: { + path: '/application-deadline', + }, + text: 'Submit completed applications and supporting documents before the final deadline.', + date: { + day: 18, + month: 'Nov', + year: 2026, + date_time: '2026-11-18', + }, + content: { + '#markup': 'Funding · Final deadline', + }, + }, +]) %} + +{% set compact_items = [ + { + title: 'Digital policy and legislation', + title_link: { + path: '/digital-policy', + }, + text: 'Current initiatives, legislation and practical guidance for Europe’s digital transformation.', + content: { + '#markup': 'Policy · 24 July 2026', + }, + badges: ['Digital', 'Policy'], + }, + { + title: 'Funding opportunities', + title_link: { + path: '/funding', + }, + text: 'Find open calls, programme information and resources for applicants and beneficiaries.', + content: { + '#markup': 'Funding · 18 July 2026', + }, + }, + { + title: 'Upcoming consultation', + title_link: { + path: '/consultation', + }, + text: 'Contribute your views to an open consultation before the submission deadline.', + content: { + '#markup': 'Consultation · Open', + }, + }, +] %} + +{% set examples = [ + { + heading: 'Default, one column with images', + variant: 'default', + columns: 1, + alignment: 'start', + image_size: 'large', + }, + { + heading: 'Default, two columns', + variant: 'default', + columns: 2, + alignment: 'start', + items: compact_items, + }, + { + heading: 'Default, three columns', + variant: 'default', + columns: 3, + alignment: 'start', + items: compact_items, + }, + { + heading: 'Highlight, one column with images', + variant: 'highlight', + columns: 1, + alignment: 'baseline', + }, + { + heading: 'Highlight, two columns with images', + variant: 'highlight', + columns: 2, + alignment: 'stretch', + }, + { + heading: 'Highlight, three columns with images', + variant: 'highlight', + columns: 3, + alignment: 'center', + }, + { + heading: 'Default date blocks, one column', + variant: 'default', + columns: 1, + alignment: 'start', + image_size: 'small', + items: date_items, + }, + { + heading: 'Default date blocks, two columns', + variant: 'default', + columns: 2, + alignment: 'start', + image_size: 'small', + items: date_items, + }, + { + heading: 'Default date blocks, three columns', + variant: 'default', + columns: 3, + alignment: 'start', + image_size: 'small', + items: date_items, + }, + { + heading: 'Highlight date blocks, one column', + variant: 'highlight', + columns: 1, + alignment: 'start', + items: date_items, + }, + { + heading: 'Highlight date blocks, two columns', + variant: 'highlight', + columns: 2, + alignment: 'start', + items: date_items, + }, + { + heading: 'Highlight date blocks, three columns', + variant: 'highlight', + columns: 3, + alignment: 'start', + items: date_items, + }, +] %} + +
+ {% for example in examples %} +
+

{{ example.heading }}

+ {% set _props = { + variant: example.variant, + columns: example.columns, + items: example.items|default(image_items), + alignment: example.alignment, + } %} + {% if example.image_size is defined %} + {% set _props = _props|merge({image_size: example.image_size}) %} + {% endif %} + {% if loop.first %} + {% set _props = _props|merge({ + title: 'Latest updates', + title_tag: 'h4', + title_link: { + path: '/latest-updates', + }, + link: { + label: 'View all updates', + path: '/updates', + icon_position: 'after', + }, + }) %} + {% endif %} + {% include 'oe_bootstrap_theme:listing' with _props only %} +
+ {% endfor %} +
diff --git a/components/listing/listing.twig b/components/listing/listing.twig new file mode 100644 index 000000000..d0ead8f60 --- /dev/null +++ b/components/listing/listing.twig @@ -0,0 +1,102 @@ +{# +/** + * @file + * Listing component. + */ +#} +{% set _icon_path = icon_path|default(bcl_icon_path()) %} +{% set _items = [] %} +{% for item in items|default([]) %} + {% set _item = item|merge({ + image_copyright_label: item.image_copyright_label|default('Image credit:'|t), + }) %} + + {% if item.title_link is not empty %} + {% set _item_title_link = item.title_link|merge({ + attributes: create_attribute(item.title_link.attributes|default({})), + }) %} + {% if item.title_link.icon is not empty %} + {% set _item_title_link = _item_title_link|merge({ + icon: item.title_link.icon|merge({ + path: item.title_link.icon.path|default(_icon_path), + attributes: create_attribute(item.title_link.icon.attributes|default({})), + }), + }) %} + {% endif %} + {% set _item = _item|merge({title_link: _item_title_link}) %} + {% endif %} + + {% if item.date is not empty %} + {% set _item = _item|merge({ + date: item.date|merge({ + icon_path: item.date.icon_path|default(_icon_path), + attributes: create_attribute(item.date.attributes|default({})), + }), + }) %} + {% endif %} + + {% if item.header is defined %} + {% set _item = _item|merge({card_header: item.header}) %} + {% endif %} + {% if item.footer is defined %} + {% set _item = _item|merge({card_footer: item.footer}) %} + {% endif %} + + {% set _badges = [] %} + {% for badge in item.badges|default([]) %} + {% if badge is iterable %} + {% set badge = badge|merge({ + icon_path: badge.icon_path|default(_icon_path), + attributes: create_attribute(badge.attributes|default({})), + }) %} + {% endif %} + {% set _badges = _badges|merge([badge]) %} + {% endfor %} + {% set _items = _items|merge([_item|merge({badges: _badges})]) %} +{% endfor %} + +{% set _title_link = title_link|default({}) %} +{% if _title_link is not empty %} + {% set _title_link = _title_link|merge({attributes: create_attribute(_title_link.attributes|default({}))}) %} + {% if _title_link.icon is not empty %} + {% set _title_link = _title_link|merge({ + icon: _title_link.icon|merge({ + path: _title_link.icon.path|default(_icon_path), + attributes: create_attribute(_title_link.icon.attributes|default({})), + }), + }) %} + {% endif %} +{% endif %} + +{% set _link = link|default({}) %} +{% if _link is not empty %} + {% set _link = _link|merge({ + attributes: create_attribute(_link.attributes|default({})), + }) %} + {% if _link.icon is not empty %} + {% set _link = _link|merge({ + icon: _link.icon|merge({ + path: _link.icon.path|default(_icon_path), + attributes: create_attribute(_link.icon.attributes|default({})), + }), + }) %} + {% endif %} +{% endif %} + +{% set _attributes = attributes|default(create_attribute()) %} +{% if _attributes.class is empty %} + {% set _attributes = _attributes.addClass('text-break') %} +{% endif %} + +{% include '@oe-bcl/listing' with { + title: title, + title_tag: title_tag|default('h2'), + title_link: _title_link, + title_attributes: title_attributes, + items: _items|bcl_card_list, + variant: variant|default('default') ~ '-' ~ columns|default(1) ~ '-col', + link: _link, + alignment: alignment, + image_size: image_size, + attributes: _attributes, +} only %} diff --git a/components/modal/modal.component.yml b/components/modal/modal.component.yml new file mode 100644 index 000000000..148cd1322 --- /dev/null +++ b/components/modal/modal.component.yml @@ -0,0 +1,174 @@ +name: Modal +description: 'Displays content in a dialog layered over the page, with an optional trigger button.' +props: + type: object + properties: + modal_id: + title: 'Modal ID' + type: string + description: 'Unique ID connecting the modal and its trigger. A unique ID is generated when omitted.' + size: + title: Size + type: string + description: 'Size of the modal.' + enum: + - sm + - lg + - xl + - fullscreen + 'meta:enum': + sm: Small + lg: Large + xl: 'Extra large' + fullscreen: Fullscreen + fullscreen_responsive: + title: 'Responsive fullscreen' + type: string + description: 'Display the modal fullscreen below the selected breakpoint.' + enum: + - sm + - md + - lg + - xl + - xxl + 'meta:enum': + sm: Small + md: Medium + lg: Large + xl: 'Extra large' + xxl: 'Extra extra large' + static_backdrop: + title: 'Static backdrop' + type: boolean + description: 'Prevent the modal from closing when its backdrop is selected.' + vertically_centered: + title: 'Vertically centered' + type: boolean + description: 'Center the modal vertically in the viewport.' + scrollable: + title: Scrollable + type: boolean + description: 'Allow the modal body to scroll independently.' + title: + title: Title + type: [string, array, object] + description: 'Title displayed in the modal header.' + title_tag: + title: 'Title tag' + type: string + description: 'The tag to use for the title. Defaults to h5.' + title_attributes: + title: 'Title attributes' + type: Drupal\Core\Template\Attribute + show_close_button: + title: 'Show close button' + type: boolean + description: 'Display a close button in the modal header. Defaults to false.' + messages: + title: Messages + type: [string, array, object] + description: 'Content displayed between the modal header and body.' + body: + title: Body + type: [string, array, object] + description: 'Main modal content.' + footer: + title: Footer + type: [string, array, object] + description: 'Optional modal footer content.' + toggle: + title: 'Toggle button' + type: object + description: 'Optional button that opens the modal.' + properties: + label: + type: [string, array, object] + variant: + type: string + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + - link + size: + type: string + enum: + - sm + - md + - lg + type: + type: string + enum: + - button + - submit + - reset + outline: + type: boolean + disabled: + type: boolean + text_nowrap: + type: boolean + assistive_text: + type: string + id: + type: string + icon: + type: [string, object] + properties: + name: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_spacing: + type: string + enum: + - sm + - md + - lg + spinner: + type: object + properties: + type: + type: string + variant: + type: string + size: + type: string + assistive_text: + type: string + attributes: + type: object + show_spinner: + type: boolean + clean_class: + type: boolean + remove_icon_spacers: + type: boolean + attributes: + type: object + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute + header_attributes: + title: 'Header attributes' + type: Drupal\Core\Template\Attribute + body_attributes: + title: 'Body attributes' + type: Drupal\Core\Template\Attribute + footer_attributes: + title: 'Footer attributes' + type: Drupal\Core\Template\Attribute diff --git a/components/modal/modal.story.twig b/components/modal/modal.story.twig new file mode 100644 index 000000000..2b0d01f4c --- /dev/null +++ b/components/modal/modal.story.twig @@ -0,0 +1,109 @@ +{% set footer %} + + +{% endset %} +{% set messages %} +
Review the information before continuing.
+{% endset %} + +
+
+

Default

+ {% include 'oe_bootstrap_theme:modal' with { + modal_id: 'modal-default', + title: 'Default modal', + show_close_button: true, + body: 'This modal uses the default width and behavior.', + footer: footer, + toggle: { + label: 'Open default modal', + }, + } only %} +
+ +
+

Small and scrollable

+ {% include 'oe_bootstrap_theme:modal' with { + modal_id: 'modal-small', + size: 'sm', + scrollable: true, + title: 'Small modal', + show_close_button: true, + body: 'A compact modal with an independently scrollable body.', + footer: footer, + toggle: { + label: 'Open small modal', + variant: 'secondary', + }, + } only %} +
+ +
+

Large and vertically centered

+ {% include 'oe_bootstrap_theme:modal' with { + modal_id: 'modal-large', + size: 'lg', + vertically_centered: true, + title: 'Large centered modal', + title_tag: 'h4', + title_attributes: create_attribute().addClass('modal-title'), + show_close_button: true, + body: 'This larger dialog is centered vertically in the viewport.', + footer: footer, + toggle: { + label: 'Open large modal', + variant: 'success', + }, + } only %} +
+ +
+

Extra large with static backdrop

+ {% include 'oe_bootstrap_theme:modal' with { + modal_id: 'modal-extra-large', + size: 'xl', + static_backdrop: true, + title: 'Extra large modal', + show_close_button: true, + messages: messages, + body: 'Selecting the backdrop does not close this modal.', + footer: footer, + toggle: { + label: 'Open extra large modal', + variant: 'info', + }, + } only %} +
+ +
+

Fullscreen

+ {% include 'oe_bootstrap_theme:modal' with { + modal_id: 'modal-fullscreen', + size: 'fullscreen', + title: 'Fullscreen modal', + show_close_button: true, + body: 'This modal occupies the complete viewport.', + footer: footer, + toggle: { + label: 'Open fullscreen modal', + variant: 'dark', + }, + } only %} +
+ +
+

Responsive fullscreen

+ {% include 'oe_bootstrap_theme:modal' with { + modal_id: 'modal-responsive', + fullscreen_responsive: 'md', + title: 'Responsive fullscreen modal', + show_close_button: true, + body: 'This modal becomes fullscreen below the medium breakpoint.', + footer: footer, + toggle: { + label: 'Open responsive modal', + outline: true, + }, + } only %} +
+
diff --git a/components/modal/modal.twig b/components/modal/modal.twig new file mode 100644 index 000000000..f535481b7 --- /dev/null +++ b/components/modal/modal.twig @@ -0,0 +1,54 @@ +{# +/** + * @file + * Modal component. + */ +#} +{% set _attributes = attributes|default(create_attribute()) %} +{% set _id = _attributes.id|default(modal_id|default('modal'|clean_unique_id)) %} +{% set _header = '' %} +{% set _title_tag = title_tag|default('h5') %} + +{% if title is not empty or show_close_button ?? false %} + {% set _header %} + {% if title is not empty %} + <{{ _title_tag }}{{ title_attributes|default(create_attribute()) }}>{{ title }} + {% endif %} + {% if show_close_button ?? false %} + {% include 'oe_bootstrap_theme:button' with { + clean_class: true, + attributes: create_attribute() + .addClass('btn-close') + .setAttribute('data-bs-dismiss', 'modal') + .setAttribute('aria-label', 'Close'|t), + } only %} + {% endif %} + {% endset %} +{% endif %} + +{% include '@oe-bcl/modal' with { + id: _id, + size: size, + fullscreen_responsive: fullscreen_responsive, + static_backdrop: static_backdrop ?? false, + vertically_centered: vertically_centered ?? false, + scrollable: scrollable ?? false, + header: _header, + messages: messages, + body: body, + footer: footer, + attributes: _attributes, + header_attributes: header_attributes|default(create_attribute()), + body_attributes: body_attributes|default(create_attribute()), + footer_attributes: footer_attributes|default(create_attribute()), +} only %} + +{% if toggle is not empty %} + {% set _toggle = toggle|merge({ + attributes: create_attribute(toggle.attributes|default({})) + .setAttribute('data-bs-toggle', 'modal') + .setAttribute('data-bs-target', '#' ~ _id) + .setAttribute('aria-controls', _id), + }) %} + {% include 'oe_bootstrap_theme:button' with _toggle only %} +{% endif %} diff --git a/components/navbar/navbar.component.yml b/components/navbar/navbar.component.yml new file mode 100644 index 000000000..c260fa4b9 --- /dev/null +++ b/components/navbar/navbar.component.yml @@ -0,0 +1,56 @@ +name: Navbar +description: 'Displays a responsive navigation bar with branding, navigation, and right-aligned content regions.' +props: + type: object + properties: + collapse_id: + title: 'Collapse ID' + type: string + description: 'ID connecting the responsive toggle and collapsible content. A unique ID is generated when omitted.' + expanded_breakpoint: + title: 'Expanded breakpoint' + type: string + description: 'Breakpoint at which the navbar is expanded. Defaults to lg.' + enum: + - sm + - md + - lg + 'meta:enum': + sm: Small + md: Medium + lg: 'Large (default)' + color_scheme: + title: 'Color scheme' + type: string + description: 'Color scheme used by the navbar. Defaults to light.' + default: light + enum: + - light + - dark + 'meta:enum': + light: 'Light (default)' + dark: Dark + background: + title: Background + type: boolean + description: 'Adds a background matching the color scheme. Defaults to true.' + default: true + disable_collapse: + title: 'Disable collapse' + type: boolean + description: 'Keeps all navbar content visible and removes the responsive toggle. Defaults to true when no expanded breakpoint is provided.' + branding: + title: 'Branding region' + type: [string, array, object] + description: 'Branding content, such as a linked site name or logo.' + navigation: + title: 'Navigation region' + type: [string, array, object] + description: 'Primary navigation content.' + right: + title: 'Right region' + type: [string, array, object] + description: 'Content aligned to the end of the navbar, such as search or account actions.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/navbar/navbar.story.twig b/components/navbar/navbar.story.twig new file mode 100644 index 000000000..d10a263e5 --- /dev/null +++ b/components/navbar/navbar.story.twig @@ -0,0 +1,52 @@ +{% set branding %} + Europa +{% endset %} + +{% set navigation %} + +{% endset %} + +{% set right %} + Sign in +{% endset %} + +
+
+

Light navbar

+ {% include 'oe_bootstrap_theme:navbar' with { + expanded_breakpoint: 'lg', + branding: branding, + navigation: navigation, + right: right, + } only %} +
+ +
+

Dark navbar

+ {% include 'oe_bootstrap_theme:navbar' with { + collapse_id: 'dark-navbar', + expanded_breakpoint: 'md', + color_scheme: 'dark', + branding: branding, + navigation: navigation, + } only %} +
+ +
+

Without background or collapse

+ {% include 'oe_bootstrap_theme:navbar' with { + expanded_breakpoint: 'sm', + background: false, + disable_collapse: true, + branding: branding, + navigation: navigation, + } only %} +
+
diff --git a/components/navbar/navbar.twig b/components/navbar/navbar.twig new file mode 100644 index 000000000..b68588a41 --- /dev/null +++ b/components/navbar/navbar.twig @@ -0,0 +1,25 @@ +{# +/** + * @file + * Navbar component. + */ +#} +{% extends '@oe-bcl/navbar' %} + +{% set color_set = color_scheme|default('light') %} +{% set background = (background ?? true) ? color_set : '' %} +{% set disable_collapse = disable_collapse ?? (expanded_breakpoint is empty) %} +{% set expand = expanded_breakpoint|default('lg') %} +{% set collapse_id = collapse_id|default('bcl-navbar'|clean_unique_id) %} + +{% block branding %} + {{- branding -}} +{% endblock %} + +{% block navigation %} + {{- navigation -}} +{% endblock %} + +{% block right %} + {{- right -}} +{% endblock %} diff --git a/components/navigation/navigation.component.yml b/components/navigation/navigation.component.yml new file mode 100644 index 000000000..e282af1b9 --- /dev/null +++ b/components/navigation/navigation.component.yml @@ -0,0 +1,295 @@ +name: Navigation +description: 'Renders links, buttons, dropdowns, and nested navigation in several Bootstrap navigation styles.' +props: + type: object + properties: + variant: + title: Variant + type: string + description: 'Navigation presentation. Defaults to default.' + default: default + enum: + - default + - tabs + - pills + - navbar + 'meta:enum': + default: Default + tabs: Tabs + pills: Pills + navbar: Navbar + orientation: + title: Orientation + type: string + description: 'Navigation orientation. Defaults to horizontal.' + enum: + - horizontal + - vertical + 'meta:enum': + horizontal: Horizontal + vertical: Vertical + navigation_id: + title: 'Navigation ID' + type: string + description: 'ID added to the navigation wrapper.' + full_width: + title: 'Full width' + type: boolean + description: 'Distribute items across the available width.' + alignment: + title: Alignment + type: string + description: 'Horizontal alignment of navigation items.' + enum: + - start + - center + - end + 'meta:enum': + start: Start + center: Center + end: End + tabs_content: + title: 'Tab content' + type: boolean + description: 'Render tab panels from each item content value.' + items: + title: 'Navigation items' + type: array + description: 'Links, buttons, dropdowns, and nested navigation items.' + items: + type: object + properties: + label: + type: [string, array, object] + path: + type: [string, object] + id: + type: string + title: + type: string + variant: + type: string + active: + type: boolean + disabled: + type: boolean + standalone: + type: boolean + button: + type: boolean + type: + type: string + text_nowrap: + type: boolean + assistive_text: + type: string + icon: + anyOf: + - type: string + - type: object + properties: + name: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_display: + type: string + enum: + - flex + - inline-flex + icon_spacing: + type: string + remove_icon_spacers: + type: boolean + spinner: + type: object + properties: + type: + type: string + variant: + type: string + size: + type: string + assistive_text: + type: string + attributes: + type: object + show_spinner: + type: boolean + content: + type: [string, array, object] + target: + type: string + dropdown: + type: boolean + link: + type: boolean + alignment: + type: string + dark_mode: + type: boolean + trigger: + type: object + properties: + label: + type: [string, array, object] + path: + type: [string, object] + variant: + type: string + size: + type: string + type: + type: string + outline: + type: boolean + disabled: + type: boolean + assistive_text: + type: string + standalone: + type: boolean + icon: + anyOf: + - type: string + - type: object + properties: + name: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + attributes: + type: object + items: + type: array + description: 'Items displayed in a dropdown navigation item.' + items: + type: object + properties: + label: + type: [string, array, object] + path: + type: [string, object] + button: + type: boolean + divider: + type: boolean + active: + type: boolean + disabled: + type: boolean + id: + type: string + title: + type: string + type: + type: string + variant: + type: string + size: + type: string + outline: + type: boolean + standalone: + type: boolean + icon: + anyOf: + - type: string + - type: object + properties: + name: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + attributes: + type: object + navigation: + type: object + description: 'Nested BCL navigation configuration.' + properties: + id: + type: string + variant: + type: string + enum: + - default + - tabs + - pills + - navbar + orientation: + type: string + enum: + - horizontal + - vertical + tabs_content: + type: boolean + full_width: + type: boolean + alignment: + type: string + attributes: + type: object + items: + type: array + items: + type: object + properties: + label: + type: [string, array, object] + path: + type: [string, object] + id: + type: string + active: + type: boolean + disabled: + type: boolean + button: + type: boolean + icon: + anyOf: + - type: string + - type: object + properties: + name: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + attributes: + type: object + attributes: + type: object + icon_path: + title: 'Icons path' + type: string + description: 'Path to the SVG icon file. Defaults to the BCL icon path.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/navigation/navigation.story.twig b/components/navigation/navigation.story.twig new file mode 100644 index 000000000..b71f3e2ed --- /dev/null +++ b/components/navigation/navigation.story.twig @@ -0,0 +1,141 @@ +{% set overview_content %} +

Overview content displayed by the active tab.

+{% endset %} +{% set details_content %} +

Detailed information displayed by the second tab.

+{% endset %} + +
+
+

Default

+ {% include 'oe_bootstrap_theme:navigation' with { + variant: 'default', + alignment: 'start', + items: [ + { label: 'Overview', path: '#overview', active: true }, + { label: 'Documents', path: '#documents' }, + { label: 'Calendar', path: '#calendar', icon: 'calendar', icon_position: 'before' }, + { label: 'Disabled', path: '#disabled', disabled: true }, + ], + } only %} +
+ +
+

Vertical pills with dropdown

+
+ {% include 'oe_bootstrap_theme:navigation' with { + variant: 'pills', + orientation: 'vertical', + navigation_id: 'navigation-pills', + items: [ + { + label: 'Profile', + path: '#profile', + active: true, + icon: 'person', + icon_position: 'before', + icon_display: 'flex', + }, + { + label: 'Settings', + path: '#settings', + icon: 'gear', + icon_position: 'before', + icon_display: 'flex', + }, + { + dropdown: true, + link: true, + id: 'navigation-dropdown', + trigger: { + label: 'More', + path: '#', + icon: 'list', + icon_position: 'before', + icon_display: 'flex', + attributes: { class: ['nav-link', 'align-items-center'] }, + }, + items: [ + { label: 'Related information', path: '#related', icon: 'link', icon_position: 'before' }, + { label: 'Download', path: '#download', icon: 'download', icon_position: 'before' }, + ], + }, + ], + } only %} +
+
+ +
+

Tabs with content

+ {% include 'oe_bootstrap_theme:navigation' with { + variant: 'tabs', + navigation_id: 'navigation-tabs', + tabs_content: true, + items: [ + { + id: 'overview-tab', + label: 'Overview', + path: '#overview-panel', + target: 'overview-panel', + content: overview_content, + active: true, + }, + { + id: 'details-tab', + label: 'Details', + path: '#details-panel', + target: 'details-panel', + content: details_content, + }, + ], + } only %} +
+ +
+

Navbar

+ {% include 'oe_bootstrap_theme:navigation' with { + variant: 'navbar', + alignment: 'center', + items: [ + { label: 'Home', path: '#home', active: true }, + { label: 'Topics', path: '#topics' }, + { label: 'Contact', path: '#contact' }, + ], + } only %} +
+ +
+

Full width, aligned to end

+ {% include 'oe_bootstrap_theme:navigation' with { + variant: 'default', + full_width: true, + alignment: 'end', + items: [ + { label: 'First item', path: '#first' }, + { label: 'Second item', path: '#second' }, + { label: 'Third item', path: '#third' }, + ], + } only %} +
+ +
+

Nested navigation

+ {% include 'oe_bootstrap_theme:navigation' with { + variant: 'default', + items: [ + { + label: 'Parent section', + path: '#parent', + navigation: { + variant: 'pills', + orientation: 'vertical', + items: [ + { label: 'Child page one', path: '#child-one' }, + { label: 'Child page two', path: '#child-two' }, + ], + }, + }, + ], + } only %} +
+
diff --git a/components/navigation/navigation.twig b/components/navigation/navigation.twig new file mode 100644 index 000000000..a4cd75b6c --- /dev/null +++ b/components/navigation/navigation.twig @@ -0,0 +1,21 @@ +{# +/** + * @file + * Navigation component. + */ +#} +{% set _variant = variant|default('default') %} + +{% include '@oe-bcl/navigation' with { + id: navigation_id, + items: items|default([])|bcl_navigation_items(icon_path), + pills: _variant == 'pills', + tabs: _variant == 'tabs', + tabs_content: tabs_content ?? false, + vertical: orientation|default('horizontal') == 'vertical', + full_width: full_width ?? false, + alignment: alignment, + nav: _variant in ['pills', 'tabs'], + navbar: _variant == 'navbar', + attributes: attributes, +} only %} diff --git a/components/offcanvas/offcanvas.component.yml b/components/offcanvas/offcanvas.component.yml new file mode 100644 index 000000000..e4dd2fd8e --- /dev/null +++ b/components/offcanvas/offcanvas.component.yml @@ -0,0 +1,178 @@ +name: Offcanvas +description: 'Displays content in a responsive panel with an optional button to show and hide it.' +props: + type: object + properties: + variant: + title: Variant + type: string + description: 'The offcanvas presentation. Defaults to default.' + enum: + - default + - search + 'meta:enum': + default: Default + search: Search + offcanvas_id: + title: 'Offcanvas ID' + type: string + description: 'ID connecting the panel, close button, and toggle. A unique ID is generated when omitted.' + title: + title: Title + type: [string, array, object] + description: 'Offcanvas title.' + title_tag: + title: 'Title tag' + type: string + description: 'HTML tag used for the title. Defaults to h2.' + title_link: + title: 'Title link' + type: object + properties: + path: + type: [string, object] + label: + type: [string, array, object] + variant: + type: string + id: + type: string + disabled: + type: boolean + icon: + anyOf: + - type: string + - type: object + properties: + name: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + icon_display: + type: string + remove_icon_spacers: + type: boolean + attributes: + type: object + title_attributes: + title: 'Title attributes' + type: Drupal\Core\Template\Attribute + placement: + title: Placement + type: string + description: 'Side from which the offcanvas opens.' + enum: + - start + - end + - top + - bottom + 'meta:enum': + start: Start + end: End + top: Top + bottom: Bottom + body: + title: Body + type: [string, array, object] + description: 'Offcanvas body content.' + body_scroll: + title: 'Body scroll' + type: boolean + description: 'Allows the page body to scroll while the offcanvas is open. Defaults to false.' + backdrop: + title: Backdrop + type: boolean + description: 'Displays a backdrop while the offcanvas is open. Defaults to false.' + with_close: + title: 'Close button' + type: boolean + description: 'Displays the close button. Defaults to true.' + close_aria_label: + title: 'Close button accessible label' + type: string + description: 'Accessible label for the close button. Defaults to Close.' + extra_classes_body: + title: 'Body classes' + type: string + description: 'Additional classes for the offcanvas body.' + extra_classes_header: + title: 'Header classes' + type: string + description: 'Additional classes for the offcanvas header.' + extra_classes_close: + title: 'Close button classes' + type: string + description: 'Additional classes for the close button.' + responsiveness: + title: Responsiveness + type: string + description: 'Breakpoint at which the offcanvas becomes static. Defaults to lg.' + enum: + - none + - sm + - md + - lg + - xl + - xxl + 'meta:enum': + none: 'Always offcanvas' + sm: Small + md: Medium + lg: 'Large (default)' + xl: 'Extra large' + xxl: 'Extra extra large' + toggle: + title: 'Toggle button' + type: object + description: 'Optional button that opens the offcanvas.' + properties: + label: + type: [string, array, object] + variant: + type: string + size: + type: string + type: + type: string + outline: + type: boolean + disabled: + type: boolean + text_nowrap: + type: boolean + assistive_text: + type: string + id: + type: string + icon: + anyOf: + - type: string + - type: object + properties: + name: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + icon_spacing: + type: string + clean_class: + type: boolean + remove_icon_spacers: + type: boolean + attributes: + type: object + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/offcanvas/offcanvas.story.twig b/components/offcanvas/offcanvas.story.twig new file mode 100644 index 000000000..7b768225c --- /dev/null +++ b/components/offcanvas/offcanvas.story.twig @@ -0,0 +1,152 @@ +{% set body %} +

Use this panel for supporting navigation, filters, or focused actions without leaving the current page.

+ View related information +{% endset %} + +{% set search_body %} +
+
+ + +
+
+ {% include 'oe_bootstrap_theme:button' with { + label: 'Search', + type: 'submit', + icon: 'search', + icon_position: 'before', + } only %} + Advanced search +
+
+{% endset %} + +
+
+

Start placement

+ {% include 'oe_bootstrap_theme:offcanvas' with { + variant: 'default', + offcanvas_id: 'offcanvas-start-demo', + placement: 'start', + responsiveness: 'none', + title: 'Navigation and information', + body: body, + extra_classes_body: 'd-block', + toggle: { + label: 'Open start panel', + icon: 'filter', + icon_position: 'before', + }, + } only %} +
+ +
+

End placement with backdrop

+ {% include 'oe_bootstrap_theme:offcanvas' with { + variant: 'default', + offcanvas_id: 'offcanvas-end-demo', + placement: 'end', + responsiveness: 'none', + backdrop: true, + title: 'Supporting actions', + body: body, + extra_classes_body: 'd-block', + toggle: { label: 'Open end panel', variant: 'secondary' }, + } only %} +
+ +
+

Search variant

+ {% include 'oe_bootstrap_theme:offcanvas' with { + variant: 'search', + offcanvas_id: 'offcanvas-search-demo', + placement: 'top', + responsiveness: 'none', + title: 'Search this site', + body: search_body, + extra_classes_header: 'border-bottom', + extra_classes_body: 'py-4', + toggle: { label: 'Open search', icon: 'search' }, + } only %} +

+ Select Open search again, use the panel close button, or press Esc to close it. +

+
+ +
+

Bottom placement

+ {% include 'oe_bootstrap_theme:offcanvas' with { + variant: 'default', + offcanvas_id: 'offcanvas-bottom-demo', + placement: 'bottom', + responsiveness: 'none', + title: 'Additional options', + body: body, + extra_classes_body: 'd-block', + toggle: { label: 'Open bottom panel', outline: true }, + } only %} +
+ +
+

Responsive from extra-extra-large screens

+ {% include 'oe_bootstrap_theme:offcanvas' with { + variant: 'default', + offcanvas_id: 'offcanvas-responsive-xxl-demo', + placement: 'end', + responsiveness: 'xxl', + title: 'Responsive filters', + body: body, + body_scroll: true, + extra_classes_body: 'd-block py-3', + toggle: { + label: 'Open responsive filters', + outline: true, + icon: 'funnel', + icon_position: 'before', + attributes: { class: ['d-xxl-none'] }, + }, + } only %} +
+ +
+

Custom options

+ {% include 'oe_bootstrap_theme:offcanvas' with { + variant: 'default', + offcanvas_id: 'offcanvas-custom-demo', + placement: 'end', + responsiveness: 'none', + title: 'Linked panel title', + title_tag: 'h4', + title_link: { + path: '/example', + icon: { + name: 'arrow-right', + transformation: 'rotate-45', + }, + icon_position: 'after', + }, + title_attributes: create_attribute().addClass('text-success'), + body: body, + body_scroll: true, + backdrop: true, + close_aria_label: 'Close custom panel', + extra_classes_header: 'border-bottom bg-light', + extra_classes_body: 'd-block py-4', + extra_classes_close: 'me-1', + attributes: create_attribute().addClass('custom-offcanvas').setAttribute('data-demo', 'custom-options'), + toggle: { + label: 'Open custom panel', + variant: 'success', + size: 'lg', + assistive_text: 'Custom offcanvas example', + icon: { + name: 'arrow-left', + transformation: 'flip-horizontal', + }, + icon_position: 'before', + icon_spacing: 'sm', + attributes: { 'data-demo-trigger': 'custom-options' }, + }, + } only %} +
+
diff --git a/components/offcanvas/offcanvas.twig b/components/offcanvas/offcanvas.twig new file mode 100644 index 000000000..7b37b30d8 --- /dev/null +++ b/components/offcanvas/offcanvas.twig @@ -0,0 +1,71 @@ +{# +/** + * @file + * Offcanvas component. + */ +#} +{% set _variant = variant|default('default') %} +{% set _attributes = attributes|default(create_attribute()) %} +{% set _offcanvas_id = offcanvas_id|default(_attributes.id|default('offcanvas'|clean_unique_id)) %} +{% set _title_attributes = title_attributes|default(create_attribute()) %} +{% set _title_link = title_link|default({}) %} +{% if _title_link is not empty %} + {% set _title_link = _title_link|merge({ + attributes: create_attribute(_title_link.attributes|default({})), + }) %} + {% if _title_link.icon is defined and _title_link.icon is not empty %} + {% set _title_link_icon = _title_link.icon is iterable ? _title_link.icon : { name: _title_link.icon } %} + {% set _title_link = _title_link|merge({ + icon: _title_link_icon|merge({ + path: _title_link_icon.path|default(bcl_icon_path()), + attributes: create_attribute(_title_link_icon.attributes|default({})), + }), + }) %} + {% endif %} +{% endif %} + +{% set _extra_classes_body = extra_classes_body|default('') %} +{% set _extra_classes_header = extra_classes_header|default('') %} +{% set _extra_classes_close = extra_classes_close|default('') %} +{% set _responsiveness = responsiveness|default('lg') %} +{% if _responsiveness == 'none' %} + {% set _responsiveness = '' %} +{% endif %} +{% if _variant == 'search' %} + {% set _attributes = _attributes.addClass('bcl-offcanvas') %} + {% set _extra_classes_body = ('d-block ' ~ _extra_classes_body)|trim %} + {% if _responsiveness is not empty %} + {% set _title_attributes = _title_attributes.addClass('mb-' ~ _responsiveness ~ '-4') %} + {% set _extra_classes_body = ('p-' ~ _responsiveness ~ '-0 ' ~ _extra_classes_body)|trim %} + {% set _extra_classes_header = ('p-' ~ _responsiveness ~ '-0 d-' ~ _responsiveness ~ '-block ' ~ _extra_classes_header)|trim %} + {% set _extra_classes_close = ('d-' ~ _responsiveness ~ '-none ' ~ _extra_classes_close)|trim %} + {% endif %} +{% endif %} + +{% include '@oe-bcl/offcanvas' with { + title: title, + title_tag: title_tag|default('h2'), + title_link: _title_link, + title_attributes: _title_attributes, + placement: placement, + id: _offcanvas_id, + body: body, + with_body_scroll: body_scroll ?? false, + with_backdrop: backdrop ?? false, + with_close: with_close ?? true, + close_aria_label: close_aria_label|default('Close'|t), + extra_classes_body: _extra_classes_body, + extra_classes_header: _extra_classes_header, + extra_classes_close: _extra_classes_close, + responsiveness: _responsiveness, + attributes: _attributes, +} only %} + +{% if toggle is not empty %} + {% set _toggle_attributes = create_attribute(toggle.attributes|default({})) + .setAttribute('data-bs-toggle', 'offcanvas') + .setAttribute('data-bs-target', '#' ~ _offcanvas_id) + .setAttribute('aria-controls', _offcanvas_id) %} + {% set _toggle = toggle|merge({ attributes: _toggle_attributes }) %} + {% include 'oe_bootstrap_theme:button' with _toggle only %} +{% endif %} diff --git a/components/pagination/pagination.component.yml b/components/pagination/pagination.component.yml new file mode 100644 index 000000000..536b0fe8a --- /dev/null +++ b/components/pagination/pagination.component.yml @@ -0,0 +1,276 @@ +name: Pagination +description: 'Displays navigation for a series of related content split across multiple pages.' +props: + type: object + properties: + variant: + title: Variant + type: string + description: 'Pagination presentation. Defaults to standard pagination.' + enum: + - default + - glossary + 'meta:enum': + default: Default + glossary: Glossary + size: + title: Size + type: string + description: 'Optional pagination size.' + enum: + - sm + - lg + 'meta:enum': + sm: Small + lg: Large + alignment: + title: Alignment + type: string + description: 'Optional pagination alignment.' + enum: + - center + - end + 'meta:enum': + center: Center + end: End + enable_prev_next_icons: + title: 'Enable previous and next icons' + type: boolean + description: 'Replaces previous and next labels with icons. Defaults to false.' + default: false + enable_first_last_icons: + title: 'Enable first and last icons' + type: boolean + description: 'Replaces first and last labels with icons. Defaults to true.' + default: true + ellipsis_before: + title: 'Ellipsis before' + type: boolean + description: 'Adds an ellipsis before the numbered items. Defaults to false.' + ellipsis_after: + title: 'Ellipsis after' + type: boolean + description: 'Adds an ellipsis after the numbered items. Defaults to false.' + first: + title: 'First link' + type: object + properties: + path: + type: [string, object] + label: + type: [string, array, object] + disabled: + type: boolean + active: + type: boolean + icon: + type: [string, object] + properties: + name: + type: string + path: + type: string + size: + type: string + enum: + - 2xs + - xs + - s + - m + - l + - xl + - 2xl + - fluid + transformation: + type: string + enum: + - rotate-90 + - rotate-180 + - rotate-270 + - flip-horizontal + - flip-vertical + attributes: + type: object + attributes: + type: object + prev: + title: 'Previous link' + type: object + properties: + path: + type: [string, object] + label: + type: [string, array, object] + disabled: + type: boolean + active: + type: boolean + icon: + type: [string, object] + properties: + name: + type: string + path: + type: string + size: + type: string + enum: + - 2xs + - xs + - s + - m + - l + - xl + - 2xl + - fluid + transformation: + type: string + enum: + - rotate-90 + - rotate-180 + - rotate-270 + - flip-horizontal + - flip-vertical + attributes: + type: object + attributes: + type: object + next: + title: 'Next link' + type: object + properties: + path: + type: [string, object] + label: + type: [string, array, object] + disabled: + type: boolean + active: + type: boolean + icon: + type: [string, object] + properties: + name: + type: string + path: + type: string + size: + type: string + enum: + - 2xs + - xs + - s + - m + - l + - xl + - 2xl + - fluid + transformation: + type: string + enum: + - rotate-90 + - rotate-180 + - rotate-270 + - flip-horizontal + - flip-vertical + attributes: + type: object + attributes: + type: object + last: + title: 'Last link' + type: object + properties: + path: + type: [string, object] + label: + type: [string, array, object] + disabled: + type: boolean + active: + type: boolean + icon: + type: [string, object] + properties: + name: + type: string + path: + type: string + size: + type: string + enum: + - 2xs + - xs + - s + - m + - l + - xl + - 2xl + - fluid + transformation: + type: string + enum: + - rotate-90 + - rotate-180 + - rotate-270 + - flip-horizontal + - flip-vertical + attributes: + type: object + attributes: + type: object + items: + title: Items + type: array + description: 'Numbered or glossary pagination items.' + items: + type: object + properties: + path: + type: [string, object] + label: + type: [string, array, object] + disabled: + type: boolean + active: + type: boolean + icon: + type: [string, object] + properties: + name: + type: string + path: + type: string + size: + type: string + enum: + - 2xs + - xs + - s + - m + - l + - xl + - 2xl + - fluid + transformation: + type: string + enum: + - rotate-90 + - rotate-180 + - rotate-270 + - flip-horizontal + - flip-vertical + attributes: + type: object + attributes: + type: object + icon_path: + title: 'Icons path' + type: string + description: 'Path to the SVG icon file. Defaults to the BCL icon path.' + list_attributes: + title: 'List attributes' + type: Drupal\Core\Template\Attribute + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/pagination/pagination.story.twig b/components/pagination/pagination.story.twig new file mode 100644 index 000000000..1f28c120d --- /dev/null +++ b/components/pagination/pagination.story.twig @@ -0,0 +1,55 @@ +{% set page_items = [ + { path: '/example?page=0', label: '1' }, + { path: '/example?page=1', label: '2', active: true }, + { path: '/example?page=2', label: '3' }, +] %} + +
+
+

Default pagination

+ {% include 'oe_bootstrap_theme:pagination' with { + size: 'sm', + alignment: 'center', + enable_first_last_icons: false, + first: { path: '/example?page=0', label: 'First' }, + prev: { path: '/example?page=0', label: 'Previous' }, + next: { path: '/example?page=2', label: 'Next' }, + last: { path: '/example?page=7', label: 'Last' }, + items: page_items, + attributes: create_attribute().setAttribute('aria-label', 'Pagination'), + } only %} +
+ +
+

Icons and ellipsis

+ {% include 'oe_bootstrap_theme:pagination' with { + alignment: 'end', + enable_prev_next_icons: true, + ellipsis_before: true, + ellipsis_after: true, + first: { path: '/example?page=0', label: 'First' }, + prev: { path: '/example?page=3', label: 'Previous' }, + next: { path: '/example?page=5', label: 'Next' }, + last: { path: '/example?page=9', label: 'Last' }, + items: [ + { path: '/example?page=4', label: '5', active: true }, + ], + attributes: create_attribute().setAttribute('aria-label', 'Pagination with skipped pages'), + } only %} +
+ +
+

Glossary pagination

+ {% include 'oe_bootstrap_theme:pagination' with { + variant: 'glossary', + items: [ + { path: '#a', label: 'A' }, + { path: '#b', label: 'B', active: true }, + { path: '#c', label: 'C' }, + { path: '#d', label: 'D' }, + { path: '#e', label: 'E' }, + { path: '#numbers', label: '0-9' }, + ], + } only %} +
+
diff --git a/components/pagination/pagination.twig b/components/pagination/pagination.twig new file mode 100644 index 000000000..f98619bd0 --- /dev/null +++ b/components/pagination/pagination.twig @@ -0,0 +1,85 @@ +{# +/** + * @file + * Pagination component. + */ +#} +{% set _variant = variant|default('default') %} +{% set _icon_path = icon_path|default(bcl_icon_path()) %} +{% set _attributes = attributes|default(create_attribute()) %} +{% set _list_attributes = list_attributes|default(create_attribute()).addClass('js-pager__items') %} + +{% if _variant == 'glossary' and not _attributes.hasAttribute('aria-label') and not _attributes.hasAttribute('aria-labelledby') %} + {% set _attributes = _attributes.setAttribute('aria-label', 'Glossary links'|t) %} +{% endif %} + +{% set _controls = [ + { item: first|default({}), label: 'First'|t }, + { item: prev|default({}), label: 'Previous'|t }, + { item: next|default({}), label: 'Next'|t }, + { item: last|default({}), label: 'Last'|t }, +] %} +{% set _processed_controls = [] %} +{% for control in _controls %} + {% set _item = control.item %} + {% if _item is not empty %} + {% set _item_attributes = create_attribute(_item.attributes|default({})) %} + {% if not _item_attributes.hasAttribute('aria-label') %} + {% set _item_attributes = _item_attributes.setAttribute('aria-label', control.label) %} + {% endif %} + {% set _item = _item|merge({ attributes: _item_attributes }) %} + {% if _item.icon is defined and _item.icon is not empty %} + {% set _item_icon = _item.icon is iterable ? _item.icon : { name: _item.icon } %} + {% set _item_icon_path = _item_icon.path|default(_icon_path) %} + {% set _item = _item|merge({ + icon_path: _item_icon_path, + icon: _item_icon|merge({ + path: _item_icon_path, + size: _item_icon.size|default('fluid'), + attributes: create_attribute(_item_icon.attributes|default({})), + }), + }) %} + {% endif %} + {% endif %} + {% set _processed_controls = _processed_controls|merge([_item]) %} +{% endfor %} + +{% set _items = [] %} +{% for item in items|default([]) %} + {% set _item_attributes = create_attribute(item.attributes|default({})) %} + {% if not _item_attributes.hasAttribute('aria-label') and item.label is not empty %} + {% set _item_attributes = _item_attributes.setAttribute('aria-label', item.label) %} + {% endif %} + {% set _item = item|merge({ attributes: _item_attributes }) %} + {% if _item.icon is defined and _item.icon is not empty %} + {% set _item_icon = _item.icon is iterable ? _item.icon : { name: _item.icon } %} + {% set _item_icon_path = _item_icon.path|default(_icon_path) %} + {% set _item = _item|merge({ + icon_path: _item_icon_path, + icon: _item_icon|merge({ + path: _item_icon_path, + size: _item_icon.size|default('fluid'), + attributes: create_attribute(_item_icon.attributes|default({})), + }), + }) %} + {% endif %} + {% set _items = _items|merge([_item]) %} +{% endfor %} + +{% include '@oe-bcl/pagination' with { + variant: _variant == 'default' ? '' : _variant, + size: size, + alignment: alignment, + enable_prev_next_icon: enable_prev_next_icons, + enable_first_last_icon: enable_first_last_icons, + ellipsis_before: ellipsis_before, + ellipsis_after: ellipsis_after, + first: _processed_controls[0], + prev: _processed_controls[1], + next: _processed_controls[2], + last: _processed_controls[3], + items: _items, + icon_path: _icon_path, + attributes: _attributes, + list_attributes: _list_attributes, +} only %} diff --git a/components/pagination_v2/pagination_v2.component.yml b/components/pagination_v2/pagination_v2.component.yml new file mode 100644 index 000000000..895738346 --- /dev/null +++ b/components/pagination_v2/pagination_v2.component.yml @@ -0,0 +1,248 @@ +name: 'Pagination v2' +description: 'Indicates a series of related content split across multiple pages, with responsive labels and optional ellipses.' +props: + type: object + properties: + size: + title: Size + type: string + description: 'Size of the pagination.' + enum: + - sm + - lg + 'meta:enum': + sm: Small + lg: Large + alignment: + title: Alignment + type: string + description: 'Alignment of the pagination on medium screens and above. Defaults to center.' + enum: + - start + - center + - end + 'meta:enum': + start: Start + center: Center + end: End + aria_label: + title: 'ARIA label' + type: string + description: 'Accessible label for the navigation wrapper.' + enable_prev_next_icon: + title: 'Previous and next icons' + type: boolean + description: 'Show icons instead of text for previous and next links. Defaults to true.' + enable_first_last_icon: + title: 'First and last icons' + type: boolean + description: 'Show icons instead of text for first and last links. Defaults to false.' + ellipsis_before: + title: 'Ellipsis before' + type: boolean + description: 'Add an ellipsis before the page items.' + ellipsis_after: + title: 'Ellipsis after' + type: boolean + description: 'Add an ellipsis after the page items.' + current_mobile_label: + title: 'Current mobile label' + type: [string, array, object] + description: 'Replacement label for the active page on small screens.' + first: + title: 'First link' + type: object + description: 'Link to the first page.' + properties: + path: + type: [string, object] + label: + type: [string, array, object] + aria_label: + type: string + disabled: + type: boolean + active: + type: boolean + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_path: + type: string + attributes: + type: object + prev: + title: 'Previous link' + type: object + description: 'Link to the previous page.' + properties: + path: + type: [string, object] + label: + type: [string, array, object] + aria_label: + type: string + disabled: + type: boolean + active: + type: boolean + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_path: + type: string + attributes: + type: object + next: + title: 'Next link' + type: object + description: 'Link to the next page.' + properties: + path: + type: [string, object] + label: + type: [string, array, object] + aria_label: + type: string + disabled: + type: boolean + active: + type: boolean + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_path: + type: string + attributes: + type: object + last: + title: 'Last link' + type: object + description: 'Link to the last page.' + properties: + path: + type: [string, object] + label: + type: [string, array, object] + aria_label: + type: string + disabled: + type: boolean + active: + type: boolean + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_path: + type: string + attributes: + type: object + items: + title: 'Pagination items' + type: array + description: 'Page links displayed by the pagination.' + items: + type: object + properties: + path: + type: [string, object] + label: + type: [string, array, object] + aria_label: + type: string + disabled: + type: boolean + active: + type: boolean + icon: + type: object + properties: + name: + type: string + size: + type: string + path: + type: string + transformation: + type: string + attributes: + type: object + icon_position: + type: string + enum: + - before + - after + icon_path: + type: string + attributes: + type: object + icon_path: + title: 'Icons path' + type: string + description: 'Path to the SVG icon file. Defaults to the BCL icon path.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute + list_attributes: + title: 'List attributes' + type: Drupal\Core\Template\Attribute diff --git a/components/pagination_v2/pagination_v2.story.twig b/components/pagination_v2/pagination_v2.story.twig new file mode 100644 index 000000000..672b9f6f8 --- /dev/null +++ b/components/pagination_v2/pagination_v2.story.twig @@ -0,0 +1,66 @@ +{% set pages = [ + { path: '#page-1', label: '1' }, + { label: '2', active: true }, + { path: '#page-3', label: '3' }, + { path: '#page-4', label: '4' }, +] %} + +
+
+

Default

+ {% include 'oe_bootstrap_theme:pagination_v2' with { + aria_label: 'Default pagination', + first: { path: '#first', label: 'First' }, + prev: { path: '#previous', label: 'Previous' }, + next: { path: '#next', label: 'Next' }, + last: { path: '#last', label: 'Last' }, + items: pages, + } only %} +
+ +
+

Icons and ellipses

+ {% include 'oe_bootstrap_theme:pagination_v2' with { + aria_label: 'Pagination with icons and ellipses', + enable_first_last_icon: true, + ellipsis_before: true, + ellipsis_after: true, + current_mobile_label: 'Page 6 of 10', + first: { path: '#first' }, + prev: { path: '#previous' }, + next: { path: '#next' }, + last: { path: '#last' }, + items: [ + { path: '#page-4', label: '4' }, + { path: '#page-5', label: '5' }, + { label: '6', active: true }, + { path: '#page-7', label: '7' }, + ], + } only %} +
+ +
+

Small, aligned to start

+ {% include 'oe_bootstrap_theme:pagination_v2' with { + aria_label: 'Small pagination', + size: 'sm', + alignment: 'start', + prev: { path: '#previous', label: 'Previous' }, + next: { path: '#next', label: 'Next' }, + items: pages, + } only %} +
+ +
+

Large, aligned to end

+ {% include 'oe_bootstrap_theme:pagination_v2' with { + aria_label: 'Large pagination', + size: 'lg', + alignment: 'end', + enable_prev_next_icon: false, + prev: { path: '#previous', label: 'Previous' }, + next: { path: '#next', label: 'Next' }, + items: pages, + } only %} +
+
diff --git a/components/pagination_v2/pagination_v2.twig b/components/pagination_v2/pagination_v2.twig new file mode 100644 index 000000000..d506dc994 --- /dev/null +++ b/components/pagination_v2/pagination_v2.twig @@ -0,0 +1,87 @@ +{# +/** + * @file + * Pagination v2 component. + */ +#} +{% set _icon_path = icon_path|default(bcl_icon_path()) %} +{% set _attributes = attributes|default(create_attribute()) %} + +{% if aria_label is not empty %} + {% set _attributes = _attributes.setAttribute('aria-label', aria_label) %} +{% endif %} + +{% set _controls = [] %} +{% for entry in [ + { item: first|default({}), aria_label: 'First'|t }, + { item: prev|default({}), aria_label: 'Previous'|t }, + { item: next|default({}), aria_label: 'Next'|t }, + { item: last|default({}), aria_label: 'Last'|t }, +] %} + {% set _control = entry.item %} + {% if _control is not empty %} + {% set _control = _control|merge({ + aria_label: _control.aria_label|default(entry.aria_label), + attributes: create_attribute(_control.attributes|default({})), + }) %} + + {% if _control.icon is defined and _control.icon is not empty %} + {% set _control = _control|merge({ + icon: _control.icon|merge({ + attributes: create_attribute(_control.icon.attributes|default({})), + }), + icon_path: _control.icon_path|default(_control.icon.path|default(_icon_path)), + }) %} + {% endif %} + {% endif %} + {% set _controls = _controls|merge([_control]) %} +{% endfor %} + +{% set _items = [] %} +{% if ellipsis_before ?? false %} + {% set _items = _items|merge([{ + item_type: 'ellipsis', + aria_label: 'Ellipsis'|t, + }]) %} +{% endif %} + +{% for item in items|default([]) %} + {% set _item = item|merge({ + aria_label: item.aria_label|default(item.label|default('')), + attributes: create_attribute(item.attributes|default({})), + }) %} + + {% if _item.icon is defined and _item.icon is not empty %} + {% set _item = _item|merge({ + icon: _item.icon|merge({ + attributes: create_attribute(_item.icon.attributes|default({})), + }), + icon_path: _item.icon_path|default(_item.icon.path|default(_icon_path)), + }) %} + {% endif %} + + {% set _items = _items|merge([_item]) %} +{% endfor %} + +{% if ellipsis_after ?? false %} + {% set _items = _items|merge([{ + item_type: 'ellipsis', + aria_label: 'Ellipsis'|t, + }]) %} +{% endif %} + +{% include '@oe-bcl/bcl-pagination-v2/pagination.html.twig' with { + size: size, + alignment: alignment|default('center'), + enable_prev_next_icon: enable_prev_next_icon ?? true, + enable_first_last_icon: enable_first_last_icon ?? false, + current_mobile_label: current_mobile_label, + first: _controls[0], + prev: _controls[1], + next: _controls[2], + last: _controls[3], + items: _items, + icon_path: _icon_path, + attributes: _attributes, + list_attributes: list_attributes|default(create_attribute()), +} only %} diff --git a/components/progress/progress.component.yml b/components/progress/progress.component.yml new file mode 100644 index 000000000..9a0e48571 --- /dev/null +++ b/components/progress/progress.component.yml @@ -0,0 +1,66 @@ +name: Progress +description: 'Indicate the progress as a bar.' +props: + type: object + properties: + label: + title: Label + type: [string, array, object] + description: 'Text before the progress bar.' + progress: + title: Progress + type: [integer, number] + description: 'The progress value for the bar.' + min: + title: Minimum + type: [integer, number, string] + description: 'Minimum value shown in the progress bar.' + max: + title: Maximum + type: [integer, number, string] + description: 'Maximum value shown in the progress bar.' + text: + title: Text + type: [string, array, object] + description: 'Text inside the progress bar.' + message: + title: Message + type: [string, array, object] + description: 'Optional description text below the progress bar.' + style: + title: 'Bar style' + type: string + description: 'Set the bar style.' + enum: + - primary + - secondary + - success + - danger + - warning + - info + - light + - dark + 'meta:enum': + primary: Primary + secondary: Secondary + success: Success + danger: Danger + warning: Warning + info: Info + light: Light + dark: Dark + striped: + title: Striped + type: boolean + description: 'Striped style for the progress bar. Defaults to false.' + animated: + title: Animated + type: boolean + description: 'Animation on striped bar. Defaults to false.' + hidden_label: + title: 'Hidden label' + type: boolean + description: 'Hides the text inside the progress bar. Defaults to false.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/progress/progress.story.twig b/components/progress/progress.story.twig new file mode 100644 index 000000000..092329c31 --- /dev/null +++ b/components/progress/progress.story.twig @@ -0,0 +1,73 @@ +{% set examples = [ + { + style: 'primary', + striped: true, + animated: true, + progress: 60, + label: 'Primary progress', + message: 'Progress bar description', + }, + { + style: 'secondary', + progress: 45, + label: 'Secondary progress', + }, + { + style: 'success', + progress: 75, + label: 'Success progress', + message: 'Most of the work is complete.', + }, + { + style: 'danger', + progress: 25, + label: 'Danger progress', + }, + { + style: 'warning', + progress: 50, + label: 'Warning progress', + }, + { + style: 'info', + progress: 65, + label: 'Info progress', + }, + { + style: 'light', + progress: 35, + label: 'Light progress', + wrapper_attributes: create_attribute().addClass(['bg-secondary', 'p-3', 'text-white']), + }, + { + style: 'dark', + progress: 85, + label: 'Dark progress', + }, + { + style: 'primary', + progress: 33.5, + label: 'Progress with hidden value', + hidden_label: true, + }, +] %} + +
+ {% for example in examples %} + {% set wrapper_attributes = example.wrapper_attributes|default(create_attribute()) %} + + {% include 'oe_bootstrap_theme:progress' with { + style: example.style, + striped: example.striped|default(false), + animated: example.animated|default(false), + hidden_label: example.hidden_label|default(false), + progress: example.progress, + min: 0, + max: 100, + label: example.label, + text: example.progress ~ '%', + message: example.message|default(''), + } only %} +
+ {% endfor %} + diff --git a/components/progress/progress.twig b/components/progress/progress.twig new file mode 100644 index 000000000..5ecb15be5 --- /dev/null +++ b/components/progress/progress.twig @@ -0,0 +1,19 @@ +{# +/** + * @file + * Progress component. + */ +#} +{% include '@oe-bcl/progress' with { + progress: progress, + min: min, + max: max, + message: message, + striped: striped, + animated: animated, + hidden_label: hidden_label, + label: label, + variant: style, + bar_label: text, + attributes: attributes, +} only %} diff --git a/components/section/section.component.yml b/components/section/section.component.yml new file mode 100644 index 000000000..d622a471d --- /dev/null +++ b/components/section/section.component.yml @@ -0,0 +1,30 @@ +name: Section +description: 'Renders content and heading inside section.' +props: + type: object + properties: + content: + title: Content + type: [string, array, object] + description: 'Content of the section.' + heading: + title: Heading + type: [string, array, object] + description: 'Heading of the section.' + heading_tag: + title: 'Heading tag' + type: string + description: 'Tag of the heading. Defaults to h2.' + tag: + title: 'Wrapper tag' + type: string + description: 'Tag of the wrapper. Defaults to section.' + heading_attributes: + title: 'Heading attributes' + type: Drupal\Core\Template\Attribute + wrapper_attributes: + title: 'Wrapper attributes' + type: Drupal\Core\Template\Attribute + attributes: + title: 'Section attributes' + type: Drupal\Core\Template\Attribute diff --git a/components/section/section.story.twig b/components/section/section.story.twig new file mode 100644 index 000000000..2696b012e --- /dev/null +++ b/components/section/section.story.twig @@ -0,0 +1,25 @@ +{% set content %} +

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+

Tellus in hac habitasse platea dictumst vestibulum rhoncus est pellentesque.

+{% endset %} + +
+ {% include 'oe_bootstrap_theme:section' with { + heading: 'This is the title of the section', + content: content, + } only %} + + {% include 'oe_bootstrap_theme:section' with { + tag: 'article', + heading_tag: 'h3', + heading: 'Section with custom tags', + content: content, + attributes: create_attribute().addClass(['content-wrapper']).setAttribute('id', 'section-id'), + heading_attributes: create_attribute().addClass(['content-heading']), + wrapper_attributes: create_attribute().addClass(['content-body']), + } only %} + + {% include 'oe_bootstrap_theme:section' with { + content: content, + } only %} +
diff --git a/components/section/section.twig b/components/section/section.twig new file mode 100644 index 000000000..bb6ffe337 --- /dev/null +++ b/components/section/section.twig @@ -0,0 +1,22 @@ +{# +/** + * @file + * Section component. + */ +#} +{% set _tag = tag|default('section') %} +{% set _heading_tag = heading_tag|default('h2') %} +{% set _attributes = attributes|default(create_attribute()) %} +{% set _heading_attributes = heading_attributes|default(create_attribute()) %} +{% set _wrapper_attributes = wrapper_attributes|default(create_attribute()) %} + +<{{ _tag }}{{ _attributes.addClass(['section']) }}> + {% if heading is not empty %} + <{{ _heading_tag }}{{ _heading_attributes.addClass(['section__title']) }}> + {{ heading }} + + {% endif %} + + {{ content }} + + diff --git a/components/timeline/timeline.component.yml b/components/timeline/timeline.component.yml new file mode 100644 index 000000000..a65e02127 --- /dev/null +++ b/components/timeline/timeline.component.yml @@ -0,0 +1,65 @@ +name: Timeline +description: 'Timeline is a flexible and powerful component for displaying a series of content ordered in time.' +props: + type: object + properties: + title: + title: Title + type: [string, array, object] + description: 'Title of the timeline block.' + title_link: + title: 'Title link' + type: object + description: 'Link applied to the timeline title.' + properties: + label: + type: [string, array, object] + path: + type: string + title_tag: + title: 'Title tag' + type: string + description: 'The tag to use for the title. Defaults to h2.' + title_attributes: + title: 'Title attributes' + type: Drupal\Core\Template\Attribute + expand_button_label: + title: 'Label expanded' + type: string + description: 'When all items are displayed, the label of the button to hide them.' + collapse_button_label: + title: 'Label collapsed' + type: string + description: 'When there are hidden items, the label of the button to expand them.' + hide_from: + title: 'Hide from' + type: integer + description: 'Indicate the number of items to display in the timeline, hide the others and add a toggle button to show them.' + hide_to: + title: 'Hide to' + type: integer + description: 'Indicate the number of bottom items to keep displayed when items are hidden. Defaults to 0.' + items: + title: Items + type: array + description: 'List of timeline items, with id, label, title and content.' + items: + type: object + properties: + id: + type: string + label: + type: [string, array, object] + title: + type: [string, array, object] + content: + type: [string, array, object] + attributes: + type: object + icon_path: + title: 'Icons path' + type: string + description: 'Path to icons.' + attributes: + title: Attributes + type: Drupal\Core\Template\Attribute diff --git a/components/timeline/timeline.story.twig b/components/timeline/timeline.story.twig new file mode 100644 index 000000000..a27cbdcf2 --- /dev/null +++ b/components/timeline/timeline.story.twig @@ -0,0 +1,67 @@ +{% set items = [ + { + label: '13 September 2017', + title: 'State of the Union', + content: [ + { + '#markup': 'President Juncker State of the Union speech', + }, + ], + }, + { + label: '28-29 September 2017', + title: 'Digital Summit', + content: [ + { + '#markup': 'Informal Digital Summit, Tallinn', + }, + ], + }, + { + label: '14 November 2017', + content: 'Strengthening European identity through education and culture.', + }, + { + label: '17 November 2017', + content: [ + { + '#markup': 'Social Summit in Gothenburg, Sweden', + }, + ], + }, + { + label: '6 December 2017', + content: [ + { + '#markup': 'Economic and Monetary Union package of proposals', + }, + ], + }, + { + label: '14-15 December 2017', + content: [ + { + '#markup': 'EU Leaders meeting on migration, Brussels', + }, + ], + }, +] %} + +{% include 'oe_bootstrap_theme:timeline' with { + title: 'Timeline heading', + title_tag: 'h3', + hide_from: 3, + hide_to: 0, + collapse_button_label: 'Display %d more', + expand_button_label: 'Hide %d items', + items: items, +} only %} + +{% include 'oe_bootstrap_theme:timeline' with { + title: 'Timeline with a linked title, no truncation', + title_tag: 'h3', + title_link: { + path: '/example', + }, + items: items, +} only %} diff --git a/components/timeline/timeline.twig b/components/timeline/timeline.twig new file mode 100644 index 000000000..a46fc8316 --- /dev/null +++ b/components/timeline/timeline.twig @@ -0,0 +1,28 @@ +{# +/** + * @file + * Timeline component. + */ +#} +{% set _items = [] %} +{% for item in items|default([]) %} + {% set _items = _items|merge([item|merge({ + attributes: create_attribute(item.attributes|default({})), + })]) %} +{% endfor %} + +{% include '@oe-bcl/timeline' with { + toggle_collapsed: collapse_button_label|default('Show more %d items'|t), + toggle_expanded: expand_button_label|default('Hide %d items'|t), + hide: { + from: hide_from, + to: hide_to|default(0), + }, + items: _items, + icon_path: icon_path|default(bcl_icon_path()), + attributes: attributes, + title: title, + title_link: title_link, + title_tag: title_tag, + title_attributes: title_attributes, +} only %} diff --git a/composer.json b/composer.json index 93ec45136..da7966c06 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "cweagans/composer-patches": "^1.7", "drupal/core": "^10.3 || ^11", "drupal/file_link": "^2.2", + "drupal/sdc_component_library": "^1.0", "drupal/ui_patterns": "^1.10", "drupal/ui_patterns_settings": "^2.3", "openeuropa/ecl-twig-loader": "^4.0" diff --git a/modules/oe_bootstrap_theme_helper/oe_bootstrap_theme_helper.services.yml b/modules/oe_bootstrap_theme_helper/oe_bootstrap_theme_helper.services.yml index 081e0f002..e7f8be463 100644 --- a/modules/oe_bootstrap_theme_helper/oe_bootstrap_theme_helper.services.yml +++ b/modules/oe_bootstrap_theme_helper/oe_bootstrap_theme_helper.services.yml @@ -6,6 +6,6 @@ services: - { name: twig.loader, priority: -100 } oe_bootstrap_theme_helper.twig_extension: class: Drupal\oe_bootstrap_theme_helper\TwigExtension\TwigExtension - arguments: ['@language_manager', '@twig', '@renderer'] + autowire: true tags: - { name: twig.extension } diff --git a/modules/oe_bootstrap_theme_helper/src/TwigExtension/TwigExtension.php b/modules/oe_bootstrap_theme_helper/src/TwigExtension/TwigExtension.php index 6985438ff..49484bfef 100644 --- a/modules/oe_bootstrap_theme_helper/src/TwigExtension/TwigExtension.php +++ b/modules/oe_bootstrap_theme_helper/src/TwigExtension/TwigExtension.php @@ -5,6 +5,8 @@ namespace Drupal\oe_bootstrap_theme_helper\TwigExtension; use Drupal\Component\Utility\Html; +use Drupal\Core\Extension\ThemeExtensionList; +use Drupal\Core\Theme\ThemeManagerInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Link; use Drupal\Core\Render\BubbleableMetadata; @@ -27,42 +29,13 @@ */ class TwigExtension extends AbstractExtension { - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface - */ - protected $languageManager; - - /** - * The renderer. - * - * @var \Drupal\Core\Render\RendererInterface - */ - protected RendererInterface $renderer; - - /** - * The Drupal Twig environment. - * - * @var \Drupal\Core\Template\TwigEnvironment - */ - protected TwigEnvironment $twigEnvironment; - - /** - * Constructs a new TwigExtension object. - * - * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager - * The language manager. - * @param \Drupal\Core\Template\TwigEnvironment $twigEnvironment - * The Drupal Twig environment. - * @param \Drupal\Core\Render\RendererInterface $renderer - * The renderer. - */ - public function __construct(LanguageManagerInterface $languageManager, TwigEnvironment $twigEnvironment, RendererInterface $renderer) { - $this->languageManager = $languageManager; - $this->twigEnvironment = $twigEnvironment; - $this->renderer = $renderer; - } + public function __construct( + protected readonly LanguageManagerInterface $languageManager, + protected readonly TwigEnvironment $twigEnvironment, + protected readonly RendererInterface $renderer, + protected readonly ThemeExtensionList $themeList, + protected readonly ThemeManagerInterface $themeManager, + ) {} /** * {@inheritdoc} @@ -70,6 +43,8 @@ public function __construct(LanguageManagerInterface $languageManager, TwigEnvir public function getFilters(): array { return [ new TwigFilter('bcl_card_list', [$this, 'bclCardList']), + new TwigFilter('bcl_description_list_items', [$this, 'bclDescriptionListItems']), + new TwigFilter('bcl_navigation_items', [$this, 'bclNavigationItems']), new TwigFilter('format_size', 'Drupal\Core\StringTranslation\ByteSizeMarkup::create'), new TwigFilter('to_file_icon', [$this, 'toFileIcon']), new TwigFilter('to_native_language', [ @@ -93,6 +68,8 @@ public function getFunctions(): array { 'needs_environment' => TRUE, ]), new TwigFunction('bcl_gallery_items', [$this, 'bclGalleryItems']), + new TwigFunction('bcl_icon_path', [$this, 'getBclIconPath']), + new TwigFunction('html_unique_id', [Html::class, 'getUniqueId']), ]; } @@ -115,6 +92,9 @@ public function bclCardList(array $items): array { foreach ($items as $item) { // Copy most of the fields. $bcl_card = $item; + if (isset($item['attributes']) && is_array($item['attributes'])) { + $bcl_card['attributes'] = new Attribute($item['attributes']); + } // Some fields need to be rewritten. if (isset($item['title'])) { $title = $item['title']; @@ -146,16 +126,28 @@ public function bclCardList(array $items): array { } $bcl_card['badges'] = []; foreach ($item['badges'] ?? [] as $badge) { - $bcl_card['badges'][] = [ + $bcl_badge = is_array($badge) ? $badge + ['background' => 'primary'] : [ 'label' => $badge, 'background' => 'primary', ]; + if (isset($bcl_badge['attributes']) && is_array($bcl_badge['attributes'])) { + $bcl_badge['attributes'] = new Attribute($bcl_badge['attributes']); + } + $bcl_card['badges'][] = $bcl_badge; } if (isset($item['image'])) { - $bcl_card['image'] = [ - 'path' => $item['image']->getSource(), - 'alt' => $item['image']->getAlt(), - ]; + if (is_array($item['image'])) { + $bcl_card['image'] = $item['image'] + [ + 'path' => $item['image']['src'] ?? '', + 'alt' => '', + ]; + } + else { + $bcl_card['image'] = [ + 'path' => $item['image']->getSource(), + 'alt' => $item['image']->getAlt(), + ]; + } } $bcl_cards[] = $bcl_card; @@ -164,6 +156,124 @@ public function bclCardList(array $items): array { return $bcl_cards; } + /** + * Processes items for the description list component. + * + * @param array $items + * The description list items. + * @param string $icon_path + * Path to the icons SVG file. Defaults to the BCL icon path. + * + * @return array + * The processed items. + */ + public function bclDescriptionListItems(array $items, ?string $icon_path = NULL): array { + if (!$icon_path) { + $icon_path = $this->getBclIconPath(); + } + + foreach ($items as &$item) { + if (empty($item['term']) || is_string($item['term'])) { + continue; + } + if (!is_array($item['term'])) { + throw new \InvalidArgumentException('Expected term to be a string or array.'); + } + foreach ($item['term'] as &$term) { + if (!empty($term['icon'])) { + $term['icon'] += ['size' => 'xs', 'path' => $icon_path]; + } + } + unset($term); + } + unset($item); + + return $items; + } + + /** + * Prepares recursive navigation items for BCL templates. + * + * @param array $items + * The navigation items. + * @param string|null $icon_path + * Path to the SVG icon file. Defaults to the BCL icon path. + * + * @return array + * The processed navigation items. + */ + public function bclNavigationItems(array $items, ?string $icon_path = NULL): array { + $icon_path = $icon_path ?: $this->getBclIconPath(); + + foreach ($items as &$item) { + $item = $this->prepareNavigationElement($item, $icon_path); + + if (isset($item['trigger']) && is_array($item['trigger'])) { + $item['trigger'] = $this->prepareNavigationElement($item['trigger'], $icon_path); + } + + if (isset($item['items']) && is_array($item['items'])) { + foreach ($item['items'] as &$dropdown_item) { + $dropdown_item = $this->prepareNavigationElement($dropdown_item, $icon_path); + } + unset($dropdown_item); + } + + if (isset($item['navigation']) && is_array($item['navigation'])) { + $variant = $item['navigation']['variant'] ?? 'default'; + $item['navigation']['pills'] = $variant === 'pills'; + $item['navigation']['tabs'] = $variant === 'tabs'; + $item['navigation']['nav'] = in_array($variant, ['pills', 'tabs'], TRUE); + $item['navigation']['navbar'] = $variant === 'navbar'; + $item['navigation']['vertical'] = ($item['navigation']['orientation'] ?? 'horizontal') === 'vertical'; + if (isset($item['navigation']['attributes']) && is_array($item['navigation']['attributes'])) { + $item['navigation']['attributes'] = new Attribute($item['navigation']['attributes']); + } + if (isset($item['navigation']['items']) && is_array($item['navigation']['items'])) { + $item['navigation']['items'] = $this->bclNavigationItems($item['navigation']['items'], $icon_path); + } + } + } + unset($item); + + return $items; + } + + /** + * Prepares attributes and icons for a BCL navigation element. + * + * @param array $item + * A navigation, dropdown, link, or button item. + * @param string $icon_path + * Path to the SVG icon file. + * + * @return array + * The processed element. + */ + protected function prepareNavigationElement(array $item, string $icon_path): array { + if (isset($item['attributes']) && is_array($item['attributes'])) { + $item['attributes'] = new Attribute($item['attributes']); + } + + if (!empty($item['icon'])) { + $icon = is_array($item['icon']) ? $item['icon'] : ['name' => $item['icon']]; + if (isset($icon['attributes']) && is_array($icon['attributes'])) { + $icon['attributes'] = new Attribute($icon['attributes']); + } + $item['icon'] = $icon + ['path' => $icon_path]; + } + + if (isset($item['spinner']['attributes']) && is_array($item['spinner']['attributes'])) { + $item['spinner']['attributes'] = new Attribute($item['spinner']['attributes']); + } + + if (array_key_exists('dark_mode', $item)) { + $item['dark'] = $item['dark_mode']; + } + + return $item; + } + /** * Get file icon class given its extension. * @@ -341,6 +451,17 @@ public function bclGalleryItems(array $items): array { return $items; } + /** + * Returns the URL to the BCL default icons SVG file. + * + * @return string + * The relative URL to the BCL icons SVG file. + */ + public function getBclIconPath(): string { + $theme = $this->themeList->get('oe_bootstrap_theme'); + return base_path() . $theme->getPath() . '/assets/icons/bcl-default-icons.svg'; + } + /** * Returns the children of an element array, optionally sorted by weight. * diff --git a/modules/oe_bootstrap_theme_helper/tests/src/Kernel/TwigExtensionTest.php b/modules/oe_bootstrap_theme_helper/tests/src/Kernel/TwigExtensionTest.php index c65370d14..104406419 100644 --- a/modules/oe_bootstrap_theme_helper/tests/src/Kernel/TwigExtensionTest.php +++ b/modules/oe_bootstrap_theme_helper/tests/src/Kernel/TwigExtensionTest.php @@ -458,6 +458,83 @@ public function testBclCardList(): void { } } + /** + * Tests BCL navigation items filter. + */ + public function testBclNavigationItems(): void { + $extension = $this->container->get('oe_bootstrap_theme_helper.twig_extension'); + $items = [ + [ + 'label' => 'Dropdown', + 'dark_mode' => TRUE, + 'attributes' => ['class' => ['dropdown-class']], + 'icon' => 'menu', + 'spinner' => [ + 'attributes' => ['class' => ['spinner-class']], + ], + 'trigger' => [ + 'label' => 'Open dropdown', + 'attributes' => ['class' => ['trigger-class']], + 'icon' => [ + 'name' => 'chevron-down', + 'attributes' => ['class' => ['trigger-icon-class']], + ], + ], + 'items' => [ + [ + 'label' => 'Dropdown item', + 'attributes' => ['class' => ['dropdown-item-class']], + ], + ], + 'navigation' => [ + 'variant' => 'tabs', + 'orientation' => 'vertical', + 'attributes' => ['class' => ['navigation-class']], + 'items' => [ + [ + 'label' => 'Nested item', + 'icon' => [ + 'name' => 'arrow-right', + 'path' => '/existing-icons.svg', + ], + ], + ], + ], + ], + ]; + + $result = $extension->bclNavigationItems($items, '/custom-icons.svg'); + $item = $result[0]; + + $this->assertInstanceOf(Attribute::class, $item['attributes']); + $this->assertSame('dropdown-class', (string) $item['attributes']['class']); + $this->assertSame([ + 'name' => 'menu', + 'path' => '/custom-icons.svg', + ], $item['icon']); + $this->assertInstanceOf(Attribute::class, $item['spinner']['attributes']); + $this->assertSame('spinner-class', (string) $item['spinner']['attributes']['class']); + $this->assertTrue($item['dark']); + + $this->assertInstanceOf(Attribute::class, $item['trigger']['attributes']); + $this->assertSame('trigger-class', (string) $item['trigger']['attributes']['class']); + $this->assertSame('/custom-icons.svg', $item['trigger']['icon']['path']); + $this->assertInstanceOf(Attribute::class, $item['trigger']['icon']['attributes']); + $this->assertSame('trigger-icon-class', (string) $item['trigger']['icon']['attributes']['class']); + $this->assertInstanceOf(Attribute::class, $item['items'][0]['attributes']); + $this->assertSame('dropdown-item-class', (string) $item['items'][0]['attributes']['class']); + + $navigation = $item['navigation']; + $this->assertFalse($navigation['pills']); + $this->assertTrue($navigation['tabs']); + $this->assertTrue($navigation['nav']); + $this->assertFalse($navigation['navbar']); + $this->assertTrue($navigation['vertical']); + $this->assertInstanceOf(Attribute::class, $navigation['attributes']); + $this->assertSame('navigation-class', (string) $navigation['attributes']['class']); + $this->assertSame('/existing-icons.svg', $navigation['items'][0]['icon']['path']); + } + /** * Provides data for testBclCardList(). * @@ -491,6 +568,12 @@ public function bclCardListDataProvider(): array { 'meta 2', ], ], + [ + 'image' => [ + 'src' => 'https://picsum.photos/1000/500/', + 'alt' => 'Alternative text for card image', + ], + ], ], 'expected' => [ [ @@ -563,6 +646,17 @@ public function bclCardListDataProvider(): array { ], ], ], + [ + 'title' => '', + 'subtitle' => [], + 'text' => [], + 'image' => [ + 'src' => 'https://picsum.photos/1000/500/', + 'alt' => 'Alternative text for card image', + 'path' => 'https://picsum.photos/1000/500/', + ], + 'badges' => [], + ], ], ]; } diff --git a/resources/sass/components/_patterns-preview.scss b/resources/sass/components/_patterns-preview.scss index 71e4ada34..7b72f6443 100644 --- a/resources/sass/components/_patterns-preview.scss +++ b/resources/sass/components/_patterns-preview.scss @@ -4,3 +4,61 @@ display: none; } } + +.components-preview__components { + .bcl-content-banner, + .bcl-banner { + width: 100%; + left: auto; + margin: 0 0 1rem 0; + &:has(+.bcl-copyright) { + margin-bottom: 0; + + .bcl-copyright { + margin-bottom: 1rem; + } + } + } + + .date-block-story__preview { + inline-size: 10rem; + max-inline-size: 100%; + } + + &::after { + display: none; + } +} +.components-preview__main { + [data-component-id="oe_bootstrap_theme:card"] { + img { + max-inline-size: 100%; + } + } + .carousel img { + max-inline-size: 100%; + } +} + +.components-preview { + background: transparent; + padding: 0; + .components-preview__wrapper { + gap: 0; + .components-preview__sidebar { + background-color: transparent; + box-shadow: none; + border-radius: 0; + } + .components-preview__components { + background-color: transparent; + border-radius: 0; + border: 0; + box-shadow: none; + .components-preview__preview { + background: transparent; + border-radius: 0; + --padding-base: 19px; + } + } + } +} diff --git a/tests/src/ComponentAssertion/BadgeComponentAssert.php b/tests/src/ComponentAssertion/BadgeComponentAssert.php new file mode 100644 index 000000000..d615ad625 --- /dev/null +++ b/tests/src/ComponentAssertion/BadgeComponentAssert.php @@ -0,0 +1,50 @@ + [ + [$this, 'assertLabel'], + ], + 'url' => [ + [$this, 'assertElementAttribute'], + '.badge', + 'href', + ], + 'title' => [ + [$this, 'assertElementAttribute'], + '.badge', + 'title', + ], + 'assistive_text' => [ + [$this, 'assertAssistiveText'], + ], + 'background' => [ + [$this, 'assertBackground'], + ], + 'rounded_pill' => [ + [$this, 'assertRoundedPill'], + ], + 'dismissible' => [ + [$this, 'assertDismissible'], + ], + 'outline' => [ + [$this, 'assertOutline'], + ], + ]; + } + +} diff --git a/tests/src/ComponentAssertion/ComponentAssertInterface.php b/tests/src/ComponentAssertion/ComponentAssertInterface.php new file mode 100644 index 000000000..48b2c64a3 --- /dev/null +++ b/tests/src/ComponentAssertion/ComponentAssertInterface.php @@ -0,0 +1,32 @@ +container->get('theme_installer')->install(['oe_bootstrap_theme']); + $this->config('system.theme')->set('default', 'oe_bootstrap_theme')->save(); + $this->container->set('theme.registry', NULL); + + // Prevent NotFoundHttpException in PathProcessorFront. + $system_site_config = \Drupal::configFactory()->getEditable('system.site'); + $system_site_config->set('page.front', '/')->save(); + } + + /** + * Tests rendering of SDC components. + * + * @param array $render + * A render array. + * @param array $assertions + * Test assertion expectations. + * + * @dataProvider componentRenderingProvider + */ + public function testComponentRendering(array $render, array $assertions): void { + $form_state = new FormState(); + $form_state->addBuildInfo('args', [$render]); + $form_state->setProgrammed(); + + $form = $this->container->get('form_builder')->buildForm($this, $form_state); + $this->assertComponentRendering($assertions, (string) $this->container->get('renderer')->renderRoot($form)); + } + + /** + * Data provider for component rendering tests. + * + * @return array + * A set of test data. + */ + public static function componentRenderingProvider(): array { + $components_path = __DIR__ . '/fixtures/markup_rendering_components'; + $test_cases = []; + + foreach (self::$componentList as $component) { + foreach (Yaml::decode(file_get_contents("{$components_path}/{$component}.yml")) as $key => $test_case) { + // Ensure unique test case keys across components. + $suffix = 0; + do { + $candidate_key = $key . ($suffix ? sprintf(' (%s)', $suffix) : ''); + $suffix++; + } while (isset($test_cases[$candidate_key])); + $test_cases[$candidate_key] = $test_case; + } + } + + return $test_cases; + } + + /** + * Runs various assertions on given HTML string via CSS selectors. + * + * Assertions array has to be provided in the following format: + * + * @code + * [ + * 'count' => [ + * "button[data-drupal-selector="button"]" => 1, + * "button svg" => 1, + * ], + * 'equals' => [ + * "button label" => "Button label", + * ], + * 'contains' => [ + * "button.btn-secondary" => "Button label", + * ], + * ] + * @endcode + * + * @param array $assertions + * Test assertions. + * @param string $html + * The rendered HTML markup. + */ + protected function assertComponentRendering(array $assertions, string $html): void { + $assertions += array_fill_keys(['count', 'equals', 'contains', 'component'], []); + + // Catch any unexpected assertion keys. Compare with an empty array to show + // only the unexpected keys in the exception message. + $this->assertEquals([], array_diff(array_keys($assertions), [ + 'count', + 'equals', + 'contains', + 'component', + ])); + + if (!empty($assertions['component'])) { + // Component assertion key should be the only one present. + $this->assertEquals(['component'], array_keys(array_filter($assertions))); + $this->assertComponentMarkup($assertions['component'], $html); + return; + } + + $crawler = new Crawler($html); + + // Assert presence of given strings. + foreach ($assertions['contains'] as $selector => $string) { + $element = $crawler->filter($selector); + $this->assertNotEmpty($element, sprintf('No elements found with selector "%s" in:%s%s', $selector, PHP_EOL, $html)); + $this->assertStringContainsString($string, $element->html(), sprintf('Failed assertion for selector "%s".', $selector)); + } + + // Assert occurrences of given elements. + foreach ($assertions['count'] as $selector => $expected) { + $this->assertCount($expected, $crawler->filter($selector), sprintf('Wrong count for selector "%s" in:%s%s', $selector, PHP_EOL, $html)); + } + + // Assert that a given element content equals a given string. + foreach ($assertions['equals'] as $selector => $expected) { + $element = $crawler->filter($selector); + $this->assertNotEmpty($element, sprintf('No elements found with selector "%s" in:%s%s.', $selector, PHP_EOL, $html)); + $this->assertSame($expected, trim($element->html()), sprintf('Failed assertion for selector "%s".', $selector)); + } + } + + /** + * Executes the component assertion class specified in the test data. + * + * @param array $assertions + * An associative array with 'class' and 'expected' keys. + * @param string $html + * The HTML markup. + */ + protected function assertComponentMarkup(array $assertions, string $html): void { + $this->assertEquals([], array_diff(array_keys($assertions), [ + 'class', + 'expected', + ])); + + $crawler = new Crawler($html); + $component_html = $crawler->filter('body > form')->html(); + + /** @var \Drupal\Tests\oe_bootstrap_theme\ComponentAssertion\ComponentAssertInterface $component_assert */ + $component_assert = new $assertions['class'](); + $component_assert->assertComponent($assertions['expected'], $component_html); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, ?array $render_array = NULL): array { + // Drupal 10.6 SDC validates Attribute props as objects, while YAML + // fixtures can only provide arrays. Normalize them for test rendering. + // @todo Remove this when Drupal 10.6 support is dropped. + if (($render_array['#type'] ?? NULL) === 'component') { + foreach ([ + 'attributes', + 'title_attributes', + 'subtitle_attributes', + 'media_attributes', + 'list_attributes', + 'content_attributes', + 'header_attributes', + 'body_attributes', + 'footer_attributes', + 'list_attributes', + 'wrapper_attributes', + 'heading_attributes', + ] as $attribute_prop) { + if (is_array($render_array['#props'][$attribute_prop] ?? NULL)) { + $render_array['#props'][$attribute_prop] = new Attribute($render_array['#props'][$attribute_prop]); + } + } + } + + $form['test'] = $render_array; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state): void { + $add_errors = function (array $element) use (&$add_errors, $form_state): void { + if (!empty($element['#set_validation_error'])) { + $label = !empty($element['#title']) ? $element['#title'] : implode('][', $element['#array_parents']); + $form_state->setError($element, t('Validation error on @label', ['@label' => $label])); + } + + foreach (Element::children($element) as $key) { + $add_errors($element[$key]); + } + }; + + $add_errors($form['test']); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void {} + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'oe_bootstrap_theme_component_rendering_test_form'; + } + +} diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/accordion.yml b/tests/src/Kernel/fixtures/markup_rendering_components/accordion.yml new file mode 100644 index 000000000..549d59421 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/accordion.yml @@ -0,0 +1,143 @@ +accordion_with_bare_layout: + render: + '#type': component + '#component': oe_bootstrap_theme:accordion + '#props': + open_item: 1 + bare_layout: true + items: + - title: 'Accordion Item #1' + content: 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium' + - title: 'Accordion Item #2' + content: 'Advice on living, working or travelling in the EU, on visas and immigration for non-EU citizens, European culture. Plus some extra text to make it a little bit longer.' + - title: 'Accordion Item #3' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' + assertions: + count: + 'div.accordion': 1 + '.bcl-heading': 0 + 'div.accordion.accordion-flush': 1 + 'div.accordion-item': 3 + 'div.accordion-item h2.accordion-header': 3 + 'div.accordion-item button.accordion-button': 3 + 'div.accordion-item div.accordion-collapse.collapse': 3 + 'div.accordion-item div.accordion-collapse.collapse.show': 1 + equals: + 'div.accordion-item:nth-child(1) button.accordion-button span': 'Accordion Item #1' + 'div.accordion-item:nth-child(2) button.accordion-button span': 'Accordion Item #2' + 'div.accordion-item:nth-child(3) button.accordion-button span': 'Accordion Item #3' + 'div.accordion-item:nth-child(1) div.accordion-body': 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium' + 'div.accordion-item:nth-child(2) div.accordion-body': 'Advice on living, working or travelling in the EU, on visas and immigration for non-EU citizens, European culture. Plus some extra text to make it a little bit longer.' + 'div.accordion-item:nth-child(3) div.accordion-body': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' +accordion_without_flush_with_title: + render: + '#type': component + '#component': oe_bootstrap_theme:accordion + '#props': + title: 'The accordion title' + open_item: 1 + bare_layout: false + items: + - title: 'Accordion Item #1' + content: 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium' + - title: 'Accordion Item #2' + content: 'Advice on living, working or travelling in the EU, on visas and immigration for non-EU citizens, European culture. Plus some extra text to make it a little bit longer.' + - title: 'Accordion Item #3' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' + assertions: + count: + 'div.accordion': 1 + '.bcl-heading': 1 + 'h2.bcl-heading': 1 + 'div.accordion.accordion-flush': 0 + 'div.accordion-item': 3 + 'div.accordion-item h2.accordion-header': 3 + 'div.accordion-item button.accordion-button': 3 + 'div.accordion-item div.accordion-collapse.collapse': 3 + 'div.accordion-item div.accordion-collapse.collapse.show': 1 + equals: + 'h2.bcl-heading': 'The accordion title' + 'div.accordion-item:nth-child(1) button.accordion-button span': 'Accordion Item #1' + 'div.accordion-item:nth-child(2) button.accordion-button span': 'Accordion Item #2' + 'div.accordion-item:nth-child(3) button.accordion-button span': 'Accordion Item #3' + 'div.accordion-item:nth-child(1) div.accordion-body': 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium' + 'div.accordion-item:nth-child(2) div.accordion-body': 'Advice on living, working or travelling in the EU, on visas and immigration for non-EU citizens, European culture. Plus some extra text to make it a little bit longer.' + 'div.accordion-item:nth-child(3) div.accordion-body': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' +accordion_with_title_tag: + render: + '#type': component + '#component': oe_bootstrap_theme:accordion + '#props': + title: 'The title tag is customisable' + title_tag: 'h5' + open_item: 1 + items: + - title: 'Accordion Item #1' + content: 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium' + - title: 'Accordion Item #2' + content: 'Advice on living, working or travelling in the EU, on visas and immigration for non-EU citizens, European culture. Plus some extra text to make it a little bit longer.' + - title: 'Accordion Item #3' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' + assertions: + count: + 'div.accordion': 1 + '.bcl-heading': 1 + 'h5.bcl-heading': 1 + 'div.accordion.accordion-flush': 0 + 'div.accordion-item': 3 + 'div.accordion-item h2.accordion-header': 3 + 'div.accordion-item button.accordion-button': 3 + 'div.accordion-item div.accordion-collapse.collapse': 3 + 'div.accordion-item div.accordion-collapse.collapse.show': 1 + equals: + 'h5.bcl-heading': 'The title tag is customisable' + 'div.accordion-item:nth-child(1) button.accordion-button span': 'Accordion Item #1' + 'div.accordion-item:nth-child(2) button.accordion-button span': 'Accordion Item #2' + 'div.accordion-item:nth-child(3) button.accordion-button span': 'Accordion Item #3' + 'div.accordion-item:nth-child(1) div.accordion-body': 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium' + 'div.accordion-item:nth-child(2) div.accordion-body': 'Advice on living, working or travelling in the EU, on visas and immigration for non-EU citizens, European culture. Plus some extra text to make it a little bit longer.' + 'div.accordion-item:nth-child(3) div.accordion-body': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' +accordion_with_actionable_buttons: + render: + '#type': component + '#component': oe_bootstrap_theme:accordion + '#props': + title: 'The accordion title' + accordion_id: test-accordion + expand_button_label: 'Expand' + collapse_button_label: 'Collapse' + open_item: 1 + bare_layout: false + items: + - title: 'Accordion Item #1' + content: 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium' + - title: 'Accordion Item #2' + content: 'Advice on living, working or travelling in the EU, on visas and immigration for non-EU citizens, European culture. Plus some extra text to make it a little bit longer.' + - title: 'Accordion Item #3' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' + assertions: + count: + 'div.accordion': 1 + '.bcl-heading': 1 + 'h2.bcl-heading': 1 + 'div.accordion.accordion-flush': 0 + 'div#accordion-test-accordion.accordion': 1 + 'button[data-action="expand"]': 1 + 'button[data-action="collapse"]': 1 + 'button[data-action="expand"][data-target="accordion-test-accordion"]': 1 + 'button[data-action="collapse"][data-target="accordion-test-accordion"]': 1 + 'div.accordion-item': 3 + 'div.accordion-item h2.accordion-header': 3 + 'div.accordion-item button.accordion-button': 3 + 'div.accordion-item div.accordion-collapse.collapse': 3 + 'div.accordion-item div.accordion-collapse.collapse.show': 1 + equals: + 'h2.bcl-heading': 'The accordion title' + 'button[data-action="expand"]': 'Expand' + 'button[data-action="collapse"]': 'Collapse' + 'div.accordion-item:nth-child(1) button.accordion-button span': 'Accordion Item #1' + 'div.accordion-item:nth-child(2) button.accordion-button span': 'Accordion Item #2' + 'div.accordion-item:nth-child(3) button.accordion-button span': 'Accordion Item #3' + 'div.accordion-item:nth-child(1) div.accordion-body': 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium' + 'div.accordion-item:nth-child(2) div.accordion-body': 'Advice on living, working or travelling in the EU, on visas and immigration for non-EU citizens, European culture. Plus some extra text to make it a little bit longer.' + 'div.accordion-item:nth-child(3) div.accordion-body': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/alert.yml b/tests/src/Kernel/fixtures/markup_rendering_components/alert.yml new file mode 100644 index 000000000..fbcd2ac27 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/alert.yml @@ -0,0 +1,65 @@ +alert_dismissible_animated: + render: + '#type': component + '#component': oe_bootstrap_theme:alert + '#props': + variant: danger + dismissible: true + animated_dismiss: true + heading: 'Well done!' + message: 'A simple alert. Check it out!' + icon: lock + assertions: + count: + 'div.alert.alert-danger.alert-dismissible.fade[role="alert"]': 1 + 'div.h4.alert-heading': 1 + 'button.btn-close': 1 + equals: + 'div.h4.alert-heading': 'Well done!' + contains: + 'div.alert-content': 'A simple alert. Check it out!' + 'svg': '#lock' +alert_dismissible_non_animated: + render: + '#type': component + '#component': oe_bootstrap_theme:alert + '#props': + variant: info + dismissible: true + animated_dismiss: false + heading: 'Well done!' + message: 'A simple alert. Check it out!' + icon: lock + assertions: + count: + 'div.alert.alert-info.alert-dismissible[role="alert"]': 1 + 'div.fade': 0 + 'div.h4.alert-heading': 1 + 'button.btn-close': 1 + equals: + 'div.h4.alert-heading': 'Well done!' + 'div.alert-content': '
Well done!
A simple alert. Check it out!' + contains: + 'svg': '#lock' +alert_not_dismissible: + render: + '#type': component + '#component': oe_bootstrap_theme:alert + '#props': + variant: secondary + dismissible: false + animated_dismiss: false + heading: 'Well done!' + message: 'A simple alert. Check it out!' + icon: search + assertions: + count: + 'div.alert-dismissible': 0 + 'button.btn-close': 0 + 'div.alert-secondary': 1 + 'div.h4.alert-heading': 1 + equals: + 'div.h4.alert-heading': 'Well done!' + 'div.alert-content': '
Well done!
A simple alert. Check it out!' + contains: + 'svg': '#search' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/badge.yml b/tests/src/Kernel/fixtures/markup_rendering_components/badge.yml new file mode 100644 index 000000000..fcfab0c52 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/badge.yml @@ -0,0 +1,97 @@ +badge_with_all_values: + render: + '#type': component + '#component': oe_bootstrap_theme:badge + '#props': + background: danger + rounded_pill: true + dismissible: true + outline: true + label: 'A simple badge!' + assistive_text: 'Unread status' + url: 'https://example.com' + title: 'The badge title' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\ComponentAssertion\BadgeComponentAssert + expected: + background: danger + rounded_pill: true + dismissible: true + outline: true + label: 'A simple badge!' + assistive_text: 'Unread status' + url: 'https://example.com' + title: 'The badge title' +badge_no_settings_sent: + render: + '#type': component + '#component': oe_bootstrap_theme:badge + '#props': + label: 'A simple badge!' + url: 'https://example.com' + title: 'The badge title' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\ComponentAssertion\BadgeComponentAssert + expected: + background: primary + rounded_pill: false + dismissible: false + outline: false + label: 'A simple badge!' + url: 'https://example.com' + title: 'The badge title' +badge_dismissible_with_rounded_pill: + render: + '#type': component + '#component': oe_bootstrap_theme:badge + '#props': + dismissible: true + rounded_pill: true + background: secondary + label: 'A simple badge!' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\ComponentAssertion\BadgeComponentAssert + expected: + dismissible: true + rounded_pill: true + background: secondary + label: 'A simple badge!' +badge_non_dismissible_without_rounded_pill: + render: + '#type': component + '#component': oe_bootstrap_theme:badge + '#props': + dismissible: false + rounded_pill: false + background: danger + label: 'A simple badge!' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\ComponentAssertion\BadgeComponentAssert + expected: + dismissible: false + rounded_pill: false + background: danger + label: 'A simple badge!' +badge_with_url: + render: + '#type': component + '#component': oe_bootstrap_theme:badge + '#props': + dismissible: true + rounded_pill: true + background: secondary + label: 'A simple badge!' + url: 'https://example.com' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\ComponentAssertion\BadgeComponentAssert + expected: + dismissible: true + rounded_pill: true + background: secondary + label: 'A simple badge!' + url: 'https://example.com' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/banner.yml b/tests/src/Kernel/fixtures/markup_rendering_components/banner.yml new file mode 100644 index 000000000..c76c788b5 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/banner.yml @@ -0,0 +1,202 @@ +banner_default_centered_with_call_to_action_full_width: + render: + '#type': component + '#component': oe_bootstrap_theme:banner + '#props': + variant: default + hero: false + full_width: true + centered: true + title: 'EU Budget for the future' + title_tag: h4 + description: 'Innovation, economy, environment and geopolitics' + call_to_action: + label: Subscribe + path: 'https://example.com' + assertions: + count: + '.bcl-banner__content h4.bcl-heading': 1 + 'div.bg-lighter.text-dark.bcl-banner': 1 + 'div.full-width': 1 + 'div.text-center': 1 + 'div.bcl-banner__content': 1 + 'a': 1 + 'div.hero': 0 + 'div.bcl-banner__image': 0 + contains: + '.bcl-banner__content p': 'Innovation, economy, environment and geopolitics' + equals: + '.bcl-banner__content h4.bcl-heading': 'EU Budget for the future' + '.bcl-banner__content a[href="https://example.com"]': Subscribe +banner_default_props_omitted: + render: + '#type': component + '#component': oe_bootstrap_theme:banner + '#props': + variant: default + title: 'EU Budget for the future' + description: 'Innovation, economy, environment and geopolitics' + assertions: + count: + 'div.bg-lighter.text-dark.bcl-banner': 1 + 'div.full-width': 0 + 'div.text-center': 0 + 'div.hero': 0 + 'div.bcl-banner__image': 0 + 'a': 0 + contains: + '.bcl-banner__content div.bcl-heading': 'EU Budget for the future' + '.bcl-banner__content p': 'Innovation, economy, environment and geopolitics' +banner_primary_hero_aligned_left_full_width: + render: + '#type': component + '#component': oe_bootstrap_theme:banner + '#props': + variant: primary + hero: true + full_width: true + centered: false + title: 'EU Budget for the future' + description: 'Innovation, economy, environment and geopolitics' + assertions: + count: + 'div.bg-primary.text-white.bcl-banner': 1 + 'div.full-width': 1 + 'div.text-center': 0 + 'div.bcl-banner__content': 1 + 'a': 0 + 'div.hero': 1 + 'div.bcl-banner__image': 0 + contains: + '.bcl-banner__content div': 'EU Budget for the future' + '.bcl-banner__content p': 'Innovation, economy, environment and geopolitics' +banner_image_with_copyright_overlay: + render: + '#type': component + '#component': oe_bootstrap_theme:banner + '#props': + variant: image + hero: false + full_width: false + centered: false + title: 'EU Budget for the future' + description: 'Innovation, economy, environment and geopolitics' + copyright: '© 2024 Example Organisation' + image: 'https://picsum.photos/1000/500/' + assertions: + count: + '.bcl-banner': 1 + 'div.bcl-banner.bg-lighter.text-dark': 1 + 'div.overlay': 1 + 'div.shade': 0 + '.bcl-copyright': 1 + 'div.bcl-banner__image[style="background-image:url(https://picsum.photos/1000/500/)"]': 1 + contains: + '.bcl-copyright': '© 2024 Example Organisation' +banner_image_with_call_to_action_link_style_without_icon: + render: + '#type': component + '#component': oe_bootstrap_theme:banner + '#props': + variant: image + full_width: true + centered: true + title: 'EU Budget for the future' + description: 'Innovation, economy, environment and geopolitics' + call_to_action: + label: Subscribe + path: 'https://example.com' + link_style: warning + image: 'https://picsum.photos/1000/500/' + assertions: + count: + 'div.bcl-banner.bg-lighter.text-dark': 1 + 'div.full-width': 1 + 'div.text-center': 1 + 'a.link-warning': 1 + 'a svg': 0 + 'div.hero': 0 + 'div.overlay': 1 + 'div.shade': 0 + 'div.bcl-banner__image[style="background-image:url(https://picsum.photos/1000/500/)"]': 1 + contains: + '.bcl-banner__content div': 'EU Budget for the future' + '.bcl-banner__content p': 'Innovation, economy, environment and geopolitics' + equals: + '.bcl-banner__content a[href="https://example.com"]': Subscribe +banner_image_shade_centered_with_call_to_action_full_width: + render: + '#type': component + '#component': oe_bootstrap_theme:banner + '#props': + variant: image_shade + hero: false + full_width: true + centered: true + title: 'EU Budget for the future' + description: 'Innovation, economy, environment and geopolitics' + call_to_action: + label: Subscribe + path: 'https://example.com' + link_style: warning + icon: + name: chevron-right + image: 'https://picsum.photos/1000/500/' + assertions: + count: + 'div.bcl-banner.bg-lighter': 1 + 'div.bcl-banner.bg-lighter.text-dark': 0 + 'div.full-width': 1 + 'div.text-center': 1 + 'a.link-warning': 1 + 'div.hero': 0 + 'div.overlay': 0 + 'div.shade': 1 + 'div.bcl-banner__image[style="background-image:url(https://picsum.photos/1000/500/)"]': 1 + contains: + '.bcl-banner__content div': 'EU Budget for the future' + '.bcl-banner__content p': 'Innovation, economy, environment and geopolitics' + '.bcl-banner__content a[href="https://example.com"]': Subscribe + 'svg': 'chevron-right' +banner_image_shade_with_shade_disabled: + render: + '#type': component + '#component': oe_bootstrap_theme:banner + '#props': + variant: image_shade + shade: false + title: 'EU Budget for the future' + description: 'Innovation, economy, environment and geopolitics' + image: 'https://picsum.photos/1000/500/' + assertions: + count: + 'div.bcl-banner.bg-lighter': 1 + 'div.bcl-banner.bg-lighter.text-dark': 0 + 'div.overlay': 1 + 'div.shade': 0 + 'div.bcl-banner__image[style="background-image:url(https://picsum.photos/1000/500/)"]': 1 + contains: + '.bcl-banner__content div': 'EU Budget for the future' + '.bcl-banner__content p': 'Innovation, economy, environment and geopolitics' +banner_with_custom_attributes_and_content_classes: + render: + '#type': component + '#component': oe_bootstrap_theme:banner + '#props': + variant: default + attributes: + class: + - custom-banner-class + id: banner-id + data-banner-attribute: banner-attr-value + title: 'EU Budget for the future' + description: 'Innovation, economy, environment and geopolitics' + content_classes: 'custom-content-class another-content-class' + assertions: + count: + 'div#banner-id': 1 + 'div.custom-banner-class.bcl-banner.bg-lighter.text-dark': 1 + 'div[data-banner-attribute="banner-attr-value"]': 1 + 'div.bcl-banner__content.custom-content-class.another-content-class': 1 + contains: + '.bcl-banner__content': 'EU Budget for the future' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/blockquote.yml b/tests/src/Kernel/fixtures/markup_rendering_components/blockquote.yml new file mode 100644 index 000000000..d6932d506 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/blockquote.yml @@ -0,0 +1,69 @@ +blockquote: + render: + '#type': component + '#component': oe_bootstrap_theme:blockquote + '#props': + title: 'The blockquote title' + title_tag: h4 + alignment: left + quote: 'A well-known quote, contained in a blockquote element.' + attribution: 'Someone famous in' + cite: 'Source title' + assertions: + count: + '.bcl-heading': 1 + 'h4.bcl-heading': 1 + 'figure.text-left': 1 + 'blockquote.blockquote': 1 + 'figcaption.blockquote-footer': 1 + 'cite[title="Source title"]': 1 + contains: + 'figcaption.blockquote-footer': 'Someone famous in' + equals: + 'h4.bcl-heading': 'The blockquote title' + 'blockquote.blockquote p': 'A well-known quote, contained in a blockquote element.' + 'figcaption.blockquote-footer': 'Someone famous in Source title' + 'cite[title="Source title"]': 'Source title' +blockquote_right_alignment_no_title_tag: + render: + '#type': component + '#component': oe_bootstrap_theme:blockquote + '#props': + title: 'The blockquote title' + alignment: end + quote: 'A well-known quote, contained in a blockquote element.' + attribution: 'Someone famous in' + cite: 'Source title' + assertions: + count: + '.bcl-heading': 1 + 'h2.bcl-heading': 1 + 'figure.text-end': 1 + 'blockquote.blockquote': 1 + 'figcaption.blockquote-footer': 1 + 'cite[title="Source title"]': 1 + contains: + 'figcaption.blockquote-footer': 'Someone famous in' + equals: + 'h2.bcl-heading': 'The blockquote title' + 'blockquote.blockquote p': 'A well-known quote, contained in a blockquote element.' + 'figcaption.blockquote-footer': 'Someone famous in Source title' + 'cite[title="Source title"]': 'Source title' +blockquote_with_no_cite_no_alignment_no_title: + render: + '#type': component + '#component': oe_bootstrap_theme:blockquote + '#props': + quote: 'A well-known quote, contained in a blockquote element.' + attribution: 'Someone famous in' + assertions: + count: + '.bcl-heading': 0 + 'figure.text-left': 1 + 'blockquote.blockquote': 1 + 'figcaption.blockquote-footer': 1 + 'cite': 0 + contains: + 'figcaption.blockquote-footer': 'Someone famous in' + equals: + 'blockquote.blockquote p': 'A well-known quote, contained in a blockquote element.' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/breadcrumb.yml b/tests/src/Kernel/fixtures/markup_rendering_components/breadcrumb.yml new file mode 100644 index 000000000..a5ccd1bfc --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/breadcrumb.yml @@ -0,0 +1,60 @@ +breadcrumb: + render: + '#type': component + '#component': oe_bootstrap_theme:breadcrumb + '#props': + items: + - path: '/index' + label: 'Home' + id: breadcrumb-home-link + attributes: + class: + - custom-class + - path: '/example' + label: 'About the European Commission' + - path: 'https://example.com' + label: 'Link title' + assertions: + count: + 'nav[aria-label="breadcrumb"]': 1 + 'ol.breadcrumb': 1 + 'li.breadcrumb-item': 3 + 'li.breadcrumb-item:nth-child(1) a.custom-class[href="/index"]#breadcrumb-home-link': 1 + 'li.breadcrumb-item:nth-child(2) a[href="/example"]': 1 + 'li.breadcrumb-item:nth-child(3) a[href="https://example.com"]': 1 + 'li.breadcrumb-item:nth-child(3).active[aria-current="page"]': 1 + 'li.breadcrumb-item:nth-child(2) a svg.d-md-none.bi.icon--s': 1 + 'svg.d-md-none.ms-0.me-2.me-2-5.bi.icon--s': 1 + equals: + 'li.breadcrumb-item:nth-child(1) a': 'Home' + 'li.breadcrumb-item:nth-child(3) a': 'Link title' + contains: + 'li.breadcrumb-item:nth-child(2) a': 'About the European Commission' + 'svg': '/icons/bcl-default-icons.svg#arrow-left' +breadcrumb_current_page_without_link: + render: + '#type': component + '#component': oe_bootstrap_theme:breadcrumb + '#props': + items: + - path: '/index' + label: 'Home' + - path: '/example' + label: 'Library' + - label: 'Current page' + assertions: + count: + 'nav[aria-label="breadcrumb"]': 1 + 'ol.breadcrumb': 1 + 'li.breadcrumb-item': 3 + 'li.breadcrumb-item:nth-child(1) a[href="/index"]': 1 + 'li.breadcrumb-item:nth-child(2) a[href="/example"]': 1 + 'li.breadcrumb-item:nth-child(3).active[aria-current="page"]': 1 + 'li.breadcrumb-item:nth-child(3) a': 0 + 'li.breadcrumb-item:nth-child(2) a svg.d-md-none.bi.icon--s': 1 + equals: + 'li.breadcrumb-item:nth-child(1) a': 'Home' + contains: + 'li.breadcrumb-item:nth-child(2) a': 'Library' + 'li.breadcrumb-item:nth-child(3)': 'Current page' + 'svg': '/icons/bcl-default-icons.svg#arrow-left' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/button.yml b/tests/src/Kernel/fixtures/markup_rendering_components/button.yml new file mode 100644 index 000000000..8d7a1896a --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/button.yml @@ -0,0 +1,220 @@ +button_dark_with_icon_after: + render: + '#type': component + '#component': oe_bootstrap_theme:button + '#props': + variant: dark + icon_position: after + type: button + label: 'Button label' + icon: arrow-right + assertions: + count: + 'button.btn': 1 + 'svg.icon--fluid': 1 + 'button.btn-dark': 1 + 'button svg': 1 + contains: + 'button.btn-dark': 'Button label' + 'svg': '#arrow-right' +button_without_icon: + render: + '#type': component + '#component': oe_bootstrap_theme:button + '#props': + variant: primary + type: button + label: 'Button label' + assertions: + count: + 'button.btn.btn-primary': 1 + 'button svg': 0 + equals: + 'button': 'Button label' +button_secondary: + render: + '#type': component + '#component': oe_bootstrap_theme:button + '#props': + variant: secondary + type: button + label: 'Button label' + assertions: + count: + 'button.btn.btn-secondary': 1 + equals: + 'button.btn-secondary': 'Button label' +button_primary_small_with_outline_and_disabled: + render: + '#type': component + '#component': oe_bootstrap_theme:button + '#props': + variant: primary + outline: true + disabled: true + size: sm + type: button + label: 'Button label' + assertions: + count: + 'button[disabled]': 1 + 'button[aria-disabled="true"]': 1 + 'button.btn.btn-outline-primary': 1 + 'button.btn-sm': 1 + equals: + 'button.btn-outline-primary': 'Button label' +button_warning_large: + render: + '#type': component + '#component': oe_bootstrap_theme:button + '#props': + variant: warning + outline: false + disabled: false + size: lg + type: button + label: 'Button label' + assertions: + count: + 'button.btn.btn-warning': 1 + 'button[disabled]': 0 + 'button.btn-lg': 1 + equals: + 'button.btn-warning': 'Button label' +button_dark_with_icon_before_and_type_reset: + render: + '#type': component + '#component': oe_bootstrap_theme:button + '#props': + variant: dark + icon_position: before + type: reset + label: 'Button label' + icon: arrow-right + assertions: + count: + 'button.btn': 1 + 'svg.icon--fluid': 1 + 'button.btn-dark': 1 + 'button svg': 1 + 'button[type=reset]': 1 + contains: + 'button.btn-dark': 'Button label' + 'svg': '#arrow-right' +button_with_assistive_text_and_spinner: + render: + '#type': component + '#component': oe_bootstrap_theme:button + '#props': + variant: primary + type: submit + label: 'Save' + assistive_text: 'Form action' + text_nowrap: true + show_spinner: true + spinner: + size: sm + assistive_text: 'Loading' + attributes: + class: + - custom-spinner-class + assertions: + count: + 'button.btn.btn-primary[type=submit]': 1 + 'button.text-nowrap': 1 + 'button.d-inline-flex.align-items-center.gap-2': 1 + 'button > span.visually-hidden': 1 + 'button > div.spinner-border.spinner-border-sm.custom-spinner-class[role="status"]': 1 + 'button > div.spinner-border span.visually-hidden': 1 + contains: + 'button.btn-primary': 'Save' + equals: + 'button > span.visually-hidden': 'Form action' + 'button > div.spinner-border span.visually-hidden': 'Loading' +button_with_id: + render: + '#type': component + '#component': oe_bootstrap_theme:button + '#props': + variant: primary + type: button + id: button-id + label: 'Button label' + assertions: + count: + 'button#button-id': 1 + 'button.btn.btn-primary': 1 +button_disabled_via_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:button + '#props': + variant: primary + type: button + label: 'Button label' + attributes: + disabled: true + assertions: + count: + 'button[disabled]': 1 + 'button[aria-disabled="true"]': 1 +button_with_icon_spacing: + render: + '#type': component + '#component': oe_bootstrap_theme:button + '#props': + variant: primary + type: button + label: 'Button label' + icon: arrow-right + icon_spacing: lg + assertions: + count: + 'button.gap-lg-2-5': 1 + contains: + 'svg': '#arrow-right' +button_with_icon_transformation: + render: + '#type': component + '#component': oe_bootstrap_theme:button + '#props': + variant: primary + type: button + label: 'Button label' + icon: + name: arrow-right + transformation: rotate-90 + attributes: + class: + - custom-icon-class + assertions: + count: + 'svg.icon--rotate-90.custom-icon-class': 1 +button_clean_class: + render: + '#type': component + '#component': oe_bootstrap_theme:button + '#props': + variant: primary + type: button + label: 'Button label' + clean_class: true + assertions: + count: + 'button': 1 + 'button.btn': 0 + 'button.btn-primary': 0 +button_icon_without_spacer: + render: + '#type': component + '#component': oe_bootstrap_theme:button + '#props': + variant: primary + type: button + label: 'Button label' + icon: arrow-right + remove_icon_spacers: true + assertions: + count: + 'svg': 1 + 'button.gap-2-5': 0 diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/card.yml b/tests/src/Kernel/fixtures/markup_rendering_components/card.yml new file mode 100644 index 000000000..3145515da --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/card.yml @@ -0,0 +1,270 @@ +card_horizontal_with_image: + render: + '#type': component + '#component': oe_bootstrap_theme:card + '#props': + variant: default + orientation: horizontal + title: 'Title card' + title_tag: 'h2' + subtitle: 'Subtitle card' + text: 'Text card with longer text which is supported' + image: + src: 'https://picsum.photos/1000/500/' + alt: 'Alternative text for card image' + meta: + - 'DIGIT' + - 'Developer' + content: + '#type': html_tag + '#tag': button + '#value': 'Custom action' + '#attributes': + class: + - btn + - btn-primary + header: 'Header of card' + footer: 'Footer of card' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\CardPatternAssert + expected: + horizontal: true + title: 'Title card' + title_tag: 'h2' + subtitle: 'Subtitle card' + description: 'Text card with longer text which is supported' + image: + src: 'https://picsum.photos/1000/500/' + alt: 'Alternative text for card image' + content: + - 'DIGIT' + - 'Developer' + - '' + header: 'Header of card' + footer: 'Footer of card' +card_vertical_with_title_link: + render: + '#type': component + '#component': oe_bootstrap_theme:card + '#props': + variant: default + orientation: vertical + title: 'Title card' + title_link: + path: 'https://example.com' + subtitle: 'Subtitle card' + text: 'Text card with longer text which is supported' + image: + src: 'https://picsum.photos/1000/500/' + alt: 'Alternative text for card image' + content: + '#type': html_tag + '#tag': button + '#value': 'Custom action' + '#attributes': + class: + - btn + - btn-primary + header: 'Header of card' + footer: 'Footer of card' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\CardPatternAssert + expected: + horizontal: false + title: 'Title card' + title_tag: 'div' + url: 'https://example.com' + subtitle: 'Subtitle card' + description: 'Text card with longer text which is supported' + image: + src: 'https://picsum.photos/1000/500/' + alt: 'Alternative text for card image' + content: + - '' + header: 'Header of card' + footer: 'Footer of card' +card_variant_search: + render: + '#type': component + '#component': oe_bootstrap_theme:card + '#props': + variant: search + title: 'Title card' + title_tag: 'h3' + text: 'Text card with longer text which is supported' + image: + src: 'https://picsum.photos/1000/500/' + alt: '' + badges: + - label: 'Category 1' + - 'Category 2' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\CardPatternAssert + expected: + variant: search + title: 'Title card' + title_tag: 'h3' + description: 'Text card with longer text which is supported' + image: + src: 'https://picsum.photos/1000/500/' + alt: '' + badges: + - label: 'Category 1' + rounded_pill: true + - 'Category 2' +card_badge_with_custom_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:card + '#props': + variant: default + title: 'Title card' + badges: + - label: 'Custom badge' + attributes: + class: + - custom-badge-class + assertions: + count: + 'span.badge.custom-badge-class': 1 +card_title_link_with_custom_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:card + '#props': + variant: default + title: 'Title card' + title_link: + path: 'https://example.com' + attributes: + class: + - custom-link-class + assertions: + count: + '.card-title a.custom-link-class[href="https://example.com"]': 1 +card_date_with_custom_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:card + '#props': + variant: default + title: 'Title card' + date: + year: '2021' + month: 'Jan' + day: '23' + attributes: + class: + - custom-date-class + assertions: + count: + 'time.custom-date-class': 1 +card_with_date_range: + render: + '#type': component + '#component': oe_bootstrap_theme:card + '#props': + variant: default + orientation: horizontal + title: 'Title card' + date: + year: "2021" + month: "Jan" + day: "23" + end_year: "2022" + end_month: "Feb" + end_day: "11" + date_time: '2021-01-23' + assertions: + count: + '.row .bcl-card-start-col': 1 + '.row .col': 1 + '.row .col-4': 0 + '.row .col-8': 0 + 'time[datetime="2021-01-23"]': 1 + equals: + 'article.card .card-title': 'Title card' + 'time > div > div > div:nth-of-type(1) .date-month': 'Jan' + 'time > div > div > div:nth-of-type(1) .date-day': '23' + 'time > div > div > div:nth-of-type(3) .date-month': 'Feb' + 'time > div > div > div:nth-of-type(3) .date-day': '11' + 'time .year-start': '2021' + 'time .year-end': '2022' +card_with_image_copyright: + render: + '#type': component + '#component': oe_bootstrap_theme:card + '#props': + variant: default + orientation: vertical + title: 'Title card' + text: 'Text card with longer text which is supported' + image: + src: 'https://picsum.photos/1000/500/' + alt: 'Alternative text for card image' + image_copyright: '© 2024 Example Organisation' + assertions: + count: + 'article.card': 1 + 'article.card img.card-img-top': 1 + 'article.card .bcl-card__image-footer .bcl-copyright': 1 + equals: + 'article.card .bcl-copyright': 'Image credit: © 2024 Example Organisation' +card_title_link_full_options: + render: + '#type': component + '#component': oe_bootstrap_theme:card + '#props': + variant: default + title: 'Title card' + title_link: + label: 'Custom link label' + path: 'https://example.com' + variant: warning + id: 'card-title-link' + disabled: true + icon: + name: arrow-right + text: 'Text card with longer text which is supported' + assertions: + count: + '.card-title a.link-warning#card-title-link.disabled[href="https://example.com"][aria-disabled="true"]': 1 + '.card-title a svg': 1 + contains: + '.card-title a': 'Custom link label' + '.card-title a svg': '#arrow-right' +card_with_image_position_bottom: + render: + '#type': component + '#component': oe_bootstrap_theme:card + '#props': + variant: default + title: 'Title card' + image: + src: 'https://picsum.photos/1000/500/' + alt: 'Alternative text for card image' + position: bottom + classes: 'custom-bottom-class' + assertions: + count: + 'article.card img.card-img-bottom.custom-bottom-class': 1 + 'article.card img.card-img-top': 0 +card_with_image_rounded: + render: + '#type': component + '#component': oe_bootstrap_theme:card + '#props': + variant: default + orientation: horizontal + title: 'Title card' + image: + src: 'https://picsum.photos/500/500/' + alt: 'Alternative text for card image' + rounded: circle + assertions: + count: + 'article.card > div.row.g-0': 1 + 'article.card img.card-img-top.rounded-circle': 1 diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/card_layout.yml b/tests/src/Kernel/fixtures/markup_rendering_components/card_layout.yml new file mode 100644 index 000000000..9ecd47391 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/card_layout.yml @@ -0,0 +1,207 @@ +card_group: + render: + '#type': component + '#component': oe_bootstrap_theme:card_layout + '#props': + variant: group + equal_height: true + items: + - title: 'Card title 1' + title_tag: 'h2' + text: 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.' + card_footer: 'Footer of card 1' + - title: 'Card title 2' + text: 'This card has supporting text below as a natural lead-in to additional content.' + card_footer: 'Footer of card 2' + - title: 'Card title 3' + text: 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.' + card_footer: 'Footer of card 3' + - title: 'Card title 4' + text: 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.' + card_footer: 'Footer of card 4' + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:card_layout"].card-group': 1 + 'article.card': 4 + 'article.card.h-100': 0 + 'div.card-title': 3 + 'h2.card-title': 1 + 'div.card-text': 4 + equals: + 'article.card:nth-child(1) h2.card-title': 'Card title 1' + 'article.card:nth-child(2) div.card-title': 'Card title 2' + 'article.card:nth-child(3) div.card-title': 'Card title 3' + 'article.card:nth-child(4) div.card-title': 'Card title 4' + 'article.card:nth-child(1) div.card-text': 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.' + 'article.card:nth-child(2) div.card-text': 'This card has supporting text below as a natural lead-in to additional content.' + 'article.card:nth-child(3) div.card-text': 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.' + 'article.card:nth-child(4) div.card-text': 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.' + 'article.card:nth-child(1) div.card-footer': 'Footer of card 1' + 'article.card:nth-child(2) div.card-footer': 'Footer of card 2' + 'article.card:nth-child(3) div.card-footer': 'Footer of card 3' + 'article.card:nth-child(4) div.card-footer': 'Footer of card 4' +card_grid_responsive_xxl_3_responsive_columns: + render: + '#type': component + '#component': oe_bootstrap_theme:card_layout + '#props': + variant: grid + responsiveness: xxl + responsive_columns: 3 + gutter: 0 + equal_height: false + items: + - title: 'Card title 1' + text: 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.' + card_footer: 'Footer of card 1' + attributes: + class: + - custom-card-class + - title: 'Card title 2' + text: 'This card has supporting text below as a natural lead-in to additional content.' + card_footer: 'Footer of card 2' + - title: 'Card title 3' + text: 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.' + card_footer: 'Footer of card 3' + - title: 'Card title 4' + text: 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.' + card_footer: 'Footer of card 4' + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:card_layout"].row': 1 + 'article.card': 4 + 'div.card-title': 4 + 'div.card-text': 4 + 'article.card.h-100': 0 + 'div.col:nth-child(1) article.card.custom-card-class': 1 + 'div.g-0': 1 + 'div.row-cols-xxl-3': 1 + equals: + 'div.col:nth-child(1) article.card div.card-title': 'Card title 1' + 'div.col:nth-child(2) article.card div.card-title': 'Card title 2' + 'div.col:nth-child(3) article.card div.card-title': 'Card title 3' + 'div.col:nth-child(4) article.card div.card-title': 'Card title 4' + 'div.col:nth-child(1) article.card div.card-text': 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.' + 'div.col:nth-child(2) article.card div.card-text': 'This card has supporting text below as a natural lead-in to additional content.' + 'div.col:nth-child(3) article.card div.card-text': 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.' + 'div.col:nth-child(4) article.card div.card-text': 'This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.' + 'div.col:nth-child(1) article.card div.card-footer': 'Footer of card 1' + 'div.col:nth-child(2) article.card div.card-footer': 'Footer of card 2' + 'div.col:nth-child(3) article.card div.card-footer': 'Footer of card 3' + 'div.col:nth-child(4) article.card div.card-footer': 'Footer of card 4' +card_grid_responsive_columns_2_md_equal_height: + render: + '#type': component + '#component': oe_bootstrap_theme:card_layout + '#props': + variant: grid + grid_columns: 2 + responsiveness: md + gutter: 2 + equal_height: true + responsive_columns: 2 + items: + - title: 'Title card 1' + subtitle: 'Subtitle card 1' + text: 'Text card with longer text which is supported 1' + image: + src: 'https://picsum.photos/1000/500/' + alt: 'Alternative text for card image' + classes: custom-image-class + rounded: '2' + image_copyright: '© 2026 Example Organisation' + badges: + - 'Category 1' + - label: 'Category 2' + background: success + content: + '#type': html_tag + '#tag': button + '#value': 'Custom action' + '#attributes': + class: + - btn + - btn-primary + card_header: 'Header of card 1' + card_footer: 'Footer of card 1' + attributes: + class: + - custom-class + - title: 'Title card 2' + subtitle: 'Subtitle card 2' + text: 'Text card with longer text which is supported 2' + image: + src: 'https://picsum.photos/1000/500/' + alt: 'Alternative text for card image' + position: bottom + classes: custom-bottom-image + content: 'Custom test' + card_header: 'Header of card 2' + card_footer: 'Footer of card 2' + - title: 'Title card 3' + subtitle: 'Subtitle card 3' + text: 'Text card with longer text which is supported 3' + image: + src: 'https://picsum.photos/1000/500/' + alt: 'Alternative text for card image' + content: 'Some representative placeholder content for the first card.' + card_header: 'Header of card 3' + card_footer: 'Footer of card 3' + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:card_layout"].row.row-cols-2.row-cols-md-2.g-2': 1 + 'article.h-100.card': 3 + 'div.col:nth-child(1) article.card.h-100.custom-class': 1 + 'img.card-img-top': 2 + 'img.card-img-top.rounded-2.custom-image-class': 1 + 'img.card-img-bottom.custom-bottom-image': 1 + 'span.badge.text-bg-primary': 1 + 'span.badge.text-bg-success': 1 + 'div.card-header': 3 + 'div.card-body': 3 + 'div.card-title': 3 + 'h6.card-subtitle': 3 + 'div.card-text': 3 + 'div.card-footer': 3 + contains: + 'div.col:nth-child(1) .bcl-copyright': '© 2026 Example Organisation' + 'div.col:nth-child(1) span.badge.text-bg-primary': 'Category 1' + 'div.col:nth-child(1) span.badge.text-bg-success': 'Category 2' + equals: + 'div.col:nth-child(1) div.card-header': 'Header of card 1' + 'div.col:nth-child(1) div.card-title': 'Title card 1' + 'div.col:nth-child(1) h6.card-subtitle': 'Subtitle card 1' + 'div.col:nth-child(1) div.card-text': 'Text card with longer text which is supported 1' + 'div.col:nth-child(1) div.card-footer': 'Footer of card 1' + 'div.col:nth-child(2) div.card-header': 'Header of card 2' + 'div.col:nth-child(2) div.card-title': 'Title card 2' + 'div.col:nth-child(2) h6.card-subtitle': 'Subtitle card 2' + 'div.col:nth-child(2) div.card-text': 'Text card with longer text which is supported 2' + 'div.col:nth-child(2) div.card-footer': 'Footer of card 2' + 'div.col:nth-child(3) div.card-header': 'Header of card 3' + 'div.col:nth-child(3) div.card-title': 'Title card 3' + 'div.col:nth-child(3) h6.card-subtitle': 'Subtitle card 3' + 'div.col:nth-child(3) div.card-text': 'Text card with longer text which is supported 3' + 'div.col:nth-child(3) div.card-footer': 'Footer of card 3' +card_masonry: + render: + '#type': component + '#component': oe_bootstrap_theme:card_layout + '#props': + variant: masonry + grid_columns: 1 + responsiveness: md + responsive_columns: 3 + gutter: 3 + items: + - title: 'Card title 1' + text: 'This is a longer card used in the masonry layout.' + - title: 'Card title 2' + text: 'Short card.' + - title: 'Card title 3' + text: 'This card has a medium amount of supporting text.' + assertions: + count: + "div[data-component-id='oe_bootstrap_theme:card_layout'][data-masonry='{\"percentPosition\": true }'].row.row-cols-1.row-cols-md-3.g-3": 1 + 'div.col': 3 + 'article.card': 3 diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/card_v2.yml b/tests/src/Kernel/fixtures/markup_rendering_components/card_v2.yml new file mode 100644 index 000000000..279111742 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/card_v2.yml @@ -0,0 +1,207 @@ +card_v2_default: + render: + '#type': component + '#component': oe_bootstrap_theme:card_v2 + '#props': + variant: default + title: + '#markup': 'New title with markup' + content: + '#markup': '

Body content card

' + footer: + '#markup': '

Footer content card

' + header: + '#markup': '

Header content card

' + media: + '#markup': 'Alternative text for card image' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\CardV2PatternAssert + expected: + variant: default + title: 'New title with markup' + tag: article + media: 'Alternative text for card image' + content: '
New title with markup

Body content card

' + footer: '

Footer content card

' + header: '

Header content card

' + attributes: + class: 'card v2' + id: ~ + media_attributes: + class: 'card-media' + id: ~ + content_attributes: + class: 'card-body' + id: ~ + header_attributes: + class: 'card-header' + id: ~ + footer_attributes: + class: 'card-footer' + id: ~ +card_v2_default_with_custom_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:card_v2 + '#props': + variant: default + title: + '#markup': 'New title with markup' + content: + '#markup': '

Body content card

' + footer: + '#markup': '

Footer content card

' + header: + '#markup': '

Header content card

' + media: + '#markup': 'Alternative text for card image' + attributes: + class: + - card-class + id: card-id + data-card-attribute: card-attr-value + media_attributes: + class: + - media-class + id: media-id + data-media-attribute: media-attr-value + content_attributes: + class: + - content-class + id: content-id + data-content-attribute: content-attr-value + header_attributes: + class: + - header-class + id: header-id + data-header-attribute: header-attr-value + footer_attributes: + class: + - footer-class + id: footer-id + data-footer-attribute: footer-attr-value + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\CardV2PatternAssert + expected: + variant: default + title: 'New title with markup' + tag: article + media: 'Alternative text for card image' + content: '
New title with markup

Body content card

' + footer: '

Footer content card

' + header: '

Header content card

' + attributes: + class: 'card-class card v2' + id: card-id + data-card-attribute: card-attr-value + media_attributes: + class: 'media-class card-media' + id: media-id + data-media-attribute: media-attr-value + content_attributes: + class: 'content-class card-body' + id: content-id + data-content-attribute: content-attr-value + header_attributes: + class: 'header-class card-header' + id: header-id + data-header-attribute: header-attr-value + footer_attributes: + class: 'footer-class card-footer' + id: footer-id + data-footer-attribute: footer-attr-value +card_v2_horizontal: + render: + '#type': component + '#component': oe_bootstrap_theme:card_v2 + '#props': + variant: horizontal + tag: div + title: + '#markup': 'New title with markup' + content: + '#markup': '

Body content card

' + footer: + '#markup': '

Footer content card

' + header: + '#markup': '

Header content card

' + media: + '#markup': 'Alternative text for card image' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\CardV2PatternAssert + expected: + variant: horizontal + title: 'New title with markup' + tag: div + media: 'Alternative text for card image' + content: '
New title with markup

Body content card

' + footer: '

Footer content card

' + header: '

Header content card

' + attributes: + class: 'card v2 horizontal' + id: ~ +card_v2_no_values: + render: + '#type': component + '#component': oe_bootstrap_theme:card_v2 + '#props': + variant: default + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\CardV2PatternAssert + expected: + variant: default + tag: article + title: ~ + media: ~ + content: '' + footer: ~ + header: ~ + attributes: + class: 'card v2' +card_v2_only_content: + render: + '#type': component + '#component': oe_bootstrap_theme:card_v2 + '#props': + variant: default + content: 'Just a simple content.' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\CardV2PatternAssert + expected: + variant: default + tag: article + title: ~ + media: ~ + content: 'Just a simple content.' + footer: ~ + header: ~ + attributes: + class: 'card v2' +card_v2_default_content_and_title: + render: + '#type': component + '#component': oe_bootstrap_theme:card_v2 + '#props': + variant: default + title: + '#markup': 'New title with markup' + content: + '#markup': '

Body content card

' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\CardV2PatternAssert + expected: + variant: default + tag: article + title: 'New title with markup' + media: ~ + content: '
New title with markup

Body content card

' + footer: ~ + header: ~ + attributes: + class: 'card v2' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/carousel.yml b/tests/src/Kernel/fixtures/markup_rendering_components/carousel.yml new file mode 100644 index 000000000..97b6f33ce --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/carousel.yml @@ -0,0 +1,193 @@ +carousel_default: + render: + '#type': component + '#component': oe_bootstrap_theme:carousel + '#props': + carousel_id: carousel-default + slides: + - caption: 'First slide caption' + caption_title: 'First slide title' + interval: 0 + image: + src: 'https://images.site.example1' + alt: 'alt image 1' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\CarouselPatternAssert + expected: + settings: + fade: false + show_controls: true + show_indicators: false + autoplay: true + disable_touch: false + items: + - image: 'alt image 1' + caption_title: 'First slide title' + caption: 'First slide caption' + +carousel_with_all_settings: + render: + '#type': component + '#component': oe_bootstrap_theme:carousel + '#props': + title: 'Carousel title' + title_tag: 'h4' + carousel_id: carousel-all-settings + fade: true + show_controls: true + show_indicators: true + autoplay: false + disable_touch: true + slides: + - image: + src: 'https://images.site.example1' + alt: 'alt image 1' + caption_title: 'First slide title' + caption: 'First slide caption' + caption_classes: 'caption_classes-1' + interval: 1 + copyright: '© 2024 Example Organisation' + link: + label: First link + path: https://link.one + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\CarouselPatternAssert + expected: + title: 'Carousel title' + title_tag: 'h4' + settings: + fade: true + show_controls: true + show_indicators: true + autoplay: false + disable_touch: true + items: + - image: 'alt image 1' + caption_title: 'First slide title' + caption: 'First slide caption' + caption_classes: 'caption_classes-1' + interval: 1 + copyright: '© 2024 Example Organisation' + link: + label: First link + path: https://link.one +carousel_settings_disabled: + render: + '#type': component + '#component': oe_bootstrap_theme:carousel + '#props': + carousel_id: carousel-settings-disabled + fade: false + show_controls: false + show_indicators: false + autoplay: false + disable_touch: false + slides: + - image: + src: 'https://images.site.example1' + alt: 'alt image 1' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\CarouselPatternAssert + expected: + settings: + fade: false + show_controls: false + show_indicators: false + autoplay: false + disable_touch: false + items: + - image: 'alt image 1' +carousel_with_fallback_id: + render: + '#type': component + '#component': oe_bootstrap_theme:carousel + '#props': + show_controls: true + show_indicators: true + slides: + - image: + src: 'https://images.site.example1' + alt: 'alt image 1' + assertions: + count: + '.carousel[id^="carousel"]': 1 + '.carousel-control-prev[data-bs-target^="#carousel"]': 1 + '.carousel-control-next[data-bs-target^="#carousel"]': 1 + '.carousel-indicators button[data-bs-target^="#carousel"]': 1 +carousel_with_user_id: + render: + '#type': component + '#component': oe_bootstrap_theme:carousel + '#props': + carousel_id: custom-carousel-id + show_controls: true + show_indicators: true + slides: + - image: + src: 'https://images.site.example1' + alt: 'alt image 1' + - image: + src: 'https://images.site.example2' + alt: 'alt image 2' + assertions: + count: + '.carousel#custom-carousel-id': 1 + '.carousel-control-prev[data-bs-target="#custom-carousel-id"]': 1 + '.carousel-control-next[data-bs-target="#custom-carousel-id"]': 1 + '.carousel-indicators button[data-bs-target="#custom-carousel-id"]': 2 +carousel_dark_mode_with_active_item: + render: + '#type': component + '#component': oe_bootstrap_theme:carousel + '#props': + carousel_id: carousel-dark-active + dark_mode: true + active_item: 2 + show_indicators: true + slides: + - image: + src: 'https://images.site.example1' + alt: 'alt image 1' + - image: + src: 'https://images.site.example2' + alt: 'alt image 2' + assertions: + count: + '.carousel.carousel-dark': 1 + '.carousel-item': 2 + '.carousel-item:nth-child(1).active': 0 + '.carousel-item:nth-child(2).active': 1 + '.carousel-indicators button:nth-child(2).active[aria-current]': 1 +carousel_with_image_attributes_and_link_icon: + render: + '#type': component + '#component': oe_bootstrap_theme:carousel + '#props': + carousel_id: carousel-image-attributes + slides: + - image: + src: 'https://images.site.example1' + alt: 'alt image 1' + title: 'Image title' + loading: lazy + width: '1000' + height: '500' + class: + - custom-image-class + caption_title: 'First slide title' + caption: 'First slide caption' + link: + label: First link + path: https://link.one + icon_position: after + icon: + name: chevron-right + assertions: + count: + '.carousel-item img[title="Image title"][loading="lazy"][width="1000"][height="500"].custom-image-class.d-block.w-100': 1 + '.carousel-caption a svg.ms-2-5': 1 + contains: + '.carousel-caption a svg': '#chevron-right' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/columns.yml b/tests/src/Kernel/fixtures/markup_rendering_components/columns.yml new file mode 100644 index 000000000..4a42f3589 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/columns.yml @@ -0,0 +1,79 @@ +columns_default: + render: + '#type': component + '#component': oe_bootstrap_theme:columns + '#props': + items: + - '#plain_text': Column 1 + - '#plain_text': Column 2 + - '#type': html_tag + '#tag': strong + '#value': 'Column 3' + - '#plain_text': Column 4 + - '#plain_text': Column 5 + - '#markup': 'Column 6' + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:columns"].columns': 1 + 'div[data-component-id="oe_bootstrap_theme:columns"][style="--columns-grid--column-count: 4;"]': 1 + 'div[data-component-id="oe_bootstrap_theme:columns"] > .columns__item': 6 + 'div[data-component-id="oe_bootstrap_theme:columns"] > .columns__item:nth-child(3) strong': 1 + 'div[data-component-id="oe_bootstrap_theme:columns"] > .columns__item:nth-child(6) a[href="/example"]': 1 + equals: + 'div[data-component-id="oe_bootstrap_theme:columns"] > .columns__item:nth-child(1)': 'Column 1' + 'div[data-component-id="oe_bootstrap_theme:columns"] > .columns__item:nth-child(3) strong': 'Column 3' + 'div[data-component-id="oe_bootstrap_theme:columns"] > .columns__item:nth-child(6) a[href="/example"]': 'Column 6' +columns_three_cols: + render: + '#type': component + '#component': oe_bootstrap_theme:columns + '#props': + items: + - '#plain_text': Column 1 + - '#plain_text': Column 2 + - '#plain_text': Column 3 + - '#plain_text': Column 4 + - '#plain_text': Column 5 + - '#plain_text': Column 6 + columns: 3 + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:columns"].columns': 1 + 'div[data-component-id="oe_bootstrap_theme:columns"][style="--columns-grid--column-count: 3;"]': 1 + 'div[data-component-id="oe_bootstrap_theme:columns"] > .columns__item': 6 +columns_section_element: + render: + '#type': component + '#component': oe_bootstrap_theme:columns + '#props': + items: + - '#plain_text': Column 1 + - '#plain_text': Column 2 + - '#plain_text': Column 3 + - '#plain_text': Column 4 + - '#plain_text': Column 5 + - '#plain_text': Column 6 + tag: section + columns: 6 + assertions: + count: + 'section[data-component-id="oe_bootstrap_theme:columns"].columns': 1 + 'section[data-component-id="oe_bootstrap_theme:columns"][style="--columns-grid--column-count: 6;"]': 1 + 'section[data-component-id="oe_bootstrap_theme:columns"] > .columns__item': 6 +columns_with_custom_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:columns + '#props': + attributes: + class: + - custom-columns-class + id: columns-id + data-columns-attribute: columns-attr-value + items: + - '#plain_text': Column 1 + - '#plain_text': Column 2 + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:columns"].columns.custom-columns-class#columns-id[data-columns-attribute="columns-attr-value"]': 1 + '.columns__item': 2 diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/content_banner.yml b/tests/src/Kernel/fixtures/markup_rendering_components/content_banner.yml new file mode 100644 index 000000000..d045d4b84 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/content_banner.yml @@ -0,0 +1,279 @@ +content_banner_white_with_all_content: + render: + '#type': component + '#component': oe_bootstrap_theme:content_banner + '#props': + background: white + meta: + - '26 May 2020' + - '1 comment' + image: + src: 'https://picsum.photos/1200/600/' + alt: 'Alternative text for content banner image' + title: 'This is the title of this page' + title_tag: h2 + action_bar: + - '#markup': '' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse volutpat ultricies massa, a dapibus libero porta nec.' + badges: + - label: 'Primary badge' + rounded_pill: true + outline: false + - label: 'Secondary badge' + rounded_pill: true + links: + - label: 'Link 1' + path: 'https://example-1.com' + - label: 'Link 2' + path: 'https://example-2.com' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\ContentBannerAssert + expected: + background: white + meta: + - '26 May 2020' + - '1 comment' + image: + src: 'https://picsum.photos/1200/600/' + alt: 'Alternative text for content banner image' + title: 'This is the title of this page' + title_tag: h2 + action_bar: '' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse volutpat ultricies massa, a dapibus libero porta nec.' + badges: + - label: 'Primary badge' + rounded_pill: true + outline: false + - label: 'Secondary badge' + rounded_pill: true + links: + - label: 'Link 1' + path: 'https://example-1.com' + - label: 'Link 2' + path: 'https://example-2.com' +content_banner_white_with_image_and_copyright: + render: + '#type': component + '#component': oe_bootstrap_theme:content_banner + '#props': + background: white + image: + src: 'https://picsum.photos/1200/600/' + alt: 'Alternative text for content banner image' + title: 'This is the title of this page' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + copyright: '© 2024 Example Organisation' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\ContentBannerAssert + expected: + background: white + image: + src: 'https://picsum.photos/1200/600/' + alt: 'Alternative text for content banner image' + image_size: lg + title: 'This is the title of this page' + title_tag: h1 + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + copyright: '© 2024 Example Organisation' +content_banner_white_without_image: + render: + '#type': component + '#component': oe_bootstrap_theme:content_banner + '#props': + title: 'This is the title of this page' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse volutpat ultricies massa, a dapibus libero porta nec.' + badges: + - label: 'Primary badge' + rounded_pill: true + - label: 'Secondary badge' + rounded_pill: true + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\ContentBannerAssert + expected: + background: white + title: 'This is the title of this page' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse volutpat ultricies massa, a dapibus libero porta nec.' + badges: + - label: 'Primary badge' + rounded_pill: true + - label: 'Secondary badge' + rounded_pill: true +content_banner_gray_with_extra_large_image: + render: + '#type': component + '#component': oe_bootstrap_theme:content_banner + '#props': + background: gray + image: + src: 'https://picsum.photos/1200/600/' + alt: 'Alternative text for content banner image' + image_size: xl + title: 'This is the title of this page' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse volutpat ultricies massa, a dapibus libero porta nec.' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\ContentBannerAssert + expected: + background: lighter + image: + src: 'https://picsum.photos/1200/600/' + alt: 'Alternative text for content banner image' + image_size: xl + title: 'This is the title of this page' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse volutpat ultricies massa, a dapibus libero porta nec.' +content_banner_gray_without_image: + render: + '#type': component + '#component': oe_bootstrap_theme:content_banner + '#props': + background: gray + meta: + - '26 May 2020' + - '1 comment' + title: 'This is the title of this page' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse volutpat ultricies massa, a dapibus libero porta nec.' + badges: + - label: 'Primary badge' + rounded_pill: true + - label: 'Secondary badge' + rounded_pill: true + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\ContentBannerAssert + expected: + background: lighter + meta: + - '26 May 2020' + - '1 comment' + title: 'This is the title of this page' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse volutpat ultricies massa, a dapibus libero porta nec.' + badges: + - label: 'Primary badge' + rounded_pill: true + - label: 'Secondary badge' + rounded_pill: true +content_banner_white_with_medium_image: + render: + '#type': component + '#component': oe_bootstrap_theme:content_banner + '#props': + image: + src: 'https://picsum.photos/1200/600/' + alt: 'Alternative text for content banner image' + image_size: md + title: 'Content banner with medium image' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\ContentBannerAssert + expected: + background: white + image: + src: 'https://picsum.photos/1200/600/' + alt: 'Alternative text for content banner image' + image_size: md + title: 'Content banner with medium image' +content_banner_badge_outline_defaults: + render: + '#type': component + '#component': oe_bootstrap_theme:content_banner + '#props': + title: 'Content banner badge defaults' + badges: + - label: 'Default outlined badge' + - label: 'Explicit solid badge' + outline: false + assertions: + count: + '.badge.badge-outline-primary': 1 + '.badge.text-bg-primary': 1 + equals: + '.badge.badge-outline-primary': 'Default outlined badge' + '.badge.text-bg-primary': 'Explicit solid badge' +content_banner_subtitle_text_and_copyright_label: + render: + '#type': component + '#component': oe_bootstrap_theme:content_banner + '#props': + image: + src: 'https://picsum.photos/1200/600/' + alt: 'Alternative text for content banner image' + copyright: '© 2026 Example Organisation' + copyright_label: 'Photo credit:' + title: 'Content banner inherited card content' + subtitle: + content: 'Content banner subtitle' + tag: h3 + classes: custom-subtitle-class + text: + content: 'Content banner card text' + tag: div + classes: custom-text-class + assertions: + count: + 'h3.card-subtitle.custom-subtitle-class': 1 + 'div.card-text.custom-text-class': 1 + '.bcl-copyright .visually-hidden': 1 + equals: + 'h3.card-subtitle': 'Content banner subtitle' + 'div.card-text': 'Content banner card text' + '.bcl-copyright': 'Photo credit: © 2026 Example Organisation' +content_banner_custom_attributes_title_link_and_date: + render: + '#type': component + '#component': oe_bootstrap_theme:content_banner + '#props': + attributes: + id: custom-content-banner + class: + - custom-content-banner-class + data-content-banner: custom-value + title: 'Linked content banner title' + title_link: + path: 'https://example.com' + title_attributes: + class: + - custom-title-class + date: + day: 23 + month: Jul + year: 2026 + date_time: '2026-07-23' + content: 'Content banner with a date block.' + assertions: + count: + '#custom-content-banner.bcl-content-banner.custom-content-banner-class': 1 + '#custom-content-banner[data-content-banner="custom-value"]': 1 + 'h1.card-title.bcl-heading.custom-title-class a[href="https://example.com"]': 1 + 'time.bcl-date-block[datetime="2026-07-23"]': 1 + equals: + 'h1.card-title.bcl-heading a': 'Linked content banner title' + 'time .date-month': Jul + 'time .date-day': '23' + 'time .year-start': '2026' +content_banner_badge_and_link_with_custom_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:content_banner + '#props': + title: 'Content banner title' + badges: + - label: 'Custom badge' + attributes: + class: + - custom-badge-class + links: + - label: 'Custom link' + path: 'https://example.com' + attributes: + class: + - custom-link-class + assertions: + count: + '.badge.custom-badge-class': 1 + 'a.custom-link-class[href="https://example.com"]': 1 + contains: + '.badge.custom-badge-class': 'Custom badge' + 'a.custom-link-class': 'Custom link' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/date_block.yml b/tests/src/Kernel/fixtures/markup_rendering_components/date_block.yml new file mode 100644 index 000000000..b3791f80d --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/date_block.yml @@ -0,0 +1,66 @@ +date_block_explicit_values_and_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:date_block + '#props': + day: 23 + month: Jul + date_time: '2026-07-23' + year: 2026 + attributes: + id: custom-date-block + class: + - custom-date-block-class + data-date-block: custom-value + assertions: + count: + 'time#custom-date-block.bcl-date-block.custom-date-block-class': 1 + 'time[data-date-block="custom-value"]': 1 + 'time[datetime="2026-07-23"]': 1 + 'time svg': 0 + equals: + 'time .date-month': Jul + 'time .date-day': '23' + 'time .year-start': '2026' +date_block_range_with_custom_icon_path: + render: + '#type': component + '#component': oe_bootstrap_theme:date_block + '#props': + day: 23 + month: Jan + date_time: '2026-01-23' + year: 2026 + end_day: 11 + end_month: Feb + end_year: 2027 + icon_path: '/custom/icons.svg' + assertions: + count: + 'time[datetime="2026-01-23"]': 1 + 'time svg': 1 + 'time .year-end': 1 + contains: + 'time svg': '/custom/icons.svg#chevron-right' + equals: + 'time > div > div > div:nth-of-type(1) .date-month': Jan + 'time > div > div > div:nth-of-type(1) .date-day': '23' + 'time > div > div > div:nth-of-type(3) .date-month': Feb + 'time > div > div > div:nth-of-type(3) .date-day': '11' + 'time .year-start': '2026' + 'time .year-end': '2027' +date_block_default_values: + render: + '#type': component + '#component': oe_bootstrap_theme:date_block + '#props': {} + assertions: + count: + 'time.bcl-date-block': 1 + 'time[datetime]': 0 + 'time svg': 0 + 'time .year-end': 0 + equals: + 'time .date-month': '' + 'time .date-day': '' + 'time .year-start': '' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/description_list.yml b/tests/src/Kernel/fixtures/markup_rendering_components/description_list.yml new file mode 100644 index 000000000..8733129af --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/description_list.yml @@ -0,0 +1,106 @@ +description_list_default: + render: + '#type': component + '#component': oe_bootstrap_theme:description_list + '#props': + title: 'The description list title' + title_tag: 'h4' + orientation: default + items: + - term: + - label: 'Single label with icon' + icon: + name: 'geo-alt-fill' + definition: 'Description of first term text goes here.' + - term: + - label: 'First label without icon' + - label: 'Second label with icon' + icon: + name: 'calendar-event' + definition: 'Description of second term text goes here.' + - term: + - label: + - '#markup': 'A label with some markup.' + definition: + - label: + - '#markup': '

First line description of third term text goes here.

' + - label: + - '#markup': 'Second line description of third term text goes here.' + - term: + - label: 'Custom icon size' + icon: + name: 'bicycle' + size: 'xl' + definition: 'This is a bigger icon.' + - term: 'A single term without icon.' + definition: 'Yet another definition text.' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\DescriptionListAssert + expected: + items: + - term: + - label: 'Single label with icon' + icon: 'geo-alt-fill' + definition: 'Description of first term text goes here.' + - term: + - label: 'First label without icon' + - label: 'Second label with icon' + icon: 'calendar-event' + definition: 'Description of second term text goes here.' + - term: + - label: + - '#markup': 'A label with some markup.' + definition: + - label: '

First line description of third term text goes here.

' + - label: 'Second line description of third term text goes here.' + - term: + - label: 'Custom icon size' + icon: + name: 'bicycle' + size: 'xl' + definition: 'This is a bigger icon.' + - term: 'A single term without icon.' + definition: 'Yet another definition text.' +description_list_horizontal: + render: + '#type': component + '#component': oe_bootstrap_theme:description_list + '#props': + title: 'The description list title' + orientation: horizontal + items: + - term: + - label: 'Single label with icon' + icon: + name: 'geo-alt-fill' + definition: 'Description of first term text goes here.' + - term: + - label: 'First label without icon' + - label: 'Second label with icon' + icon: + name: 'calendar-event' + definition: 'Description of second term text goes here.' + - term: + - label: 'A single term without icon.' + definition: + - label: 'First definition for third term.' + - label: 'Second definition for third term.' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\DescriptionListAssert + expected: + items: + - term: + - label: 'Single label with icon' + icon: 'geo-alt-fill' + definition: 'Description of first term text goes here.' + - term: + - label: 'First label without icon' + - label: 'Second label with icon' + icon: 'calendar-event' + definition: 'Description of second term text goes here.' + - term: 'A single term without icon.' + definition: + - label: 'First definition for third term.' + - label: 'Second definition for third term.' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/dropdown.yml b/tests/src/Kernel/fixtures/markup_rendering_components/dropdown.yml new file mode 100644 index 000000000..2805aeff6 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/dropdown.yml @@ -0,0 +1,123 @@ +dropdown_button_default: + render: + '#type': component + '#component': oe_bootstrap_theme:dropdown + '#props': + dropdown_id: bcl-dropdown + icon_path: /custom-icons.svg + trigger: + label: 'Dropdown toggle' + variant: secondary + attributes: + class: + - custom-trigger + icon: + name: list + attributes: + class: + - custom-trigger-icon + attributes: + class: + - custom-menu + items: + - label: 'A link' + path: '/example' + attributes: + class: + - custom-link + icon: link + - label: 'A button' + button: true + attributes: + class: + - custom-button + - divider: true + - label: 'A disabled button' + button: true + disabled: true + - label: 'An active button' + button: true + active: true + assertions: + count: + 'div.dropdown': 1 + 'button#bcl-dropdown.btn-secondary.dropdown-toggle.custom-trigger[data-bs-toggle="dropdown"][autocomplete="off"]': 1 + 'button#bcl-dropdown[aria-expanded="false"]': 1 + 'ul.dropdown-menu.custom-menu[aria-labelledby="bcl-dropdown"]': 1 + 'ul.dropdown-menu-dark': 0 + 'li': 5 + 'hr.dropdown-divider': 1 + 'a.dropdown-item.custom-link[href="/example"]': 1 + 'button.dropdown-item.custom-button': 1 + 'button.dropdown-item.active': 1 + 'button.dropdown-item[disabled][aria-disabled="true"]': 1 + 'svg.custom-trigger-icon': 1 + contains: + 'button#bcl-dropdown svg': '/custom-icons.svg#list' + 'a.dropdown-item': 'A link' + 'a.dropdown-item svg': '/custom-icons.svg#link' + equals: + 'button.dropdown-item.active': 'An active button' + 'button.dropdown-item[disabled]': 'A disabled button' + 'ul.dropdown-menu li:nth-child(2) button.dropdown-item': 'A button' +dropdown_dark_aligned_dropup: + render: + '#type': component + '#component': oe_bootstrap_theme:dropdown + '#props': + dropdown_id: dark-dropdown + trigger: + label: 'Dark dropdown' + disabled: true + dark_mode: true + direction: dropup + alignment: dropdown-menu-lg-end + items: + - label: 'A link' + path: '/example' + assertions: + count: + 'div.dropdown.dropup': 1 + 'button#dark-dropdown.dropdown-toggle[disabled][aria-disabled="true"]': 1 + 'ul.dropdown-menu.dropdown-menu-dark.dropdown-menu-lg-end': 1 + 'a.dropdown-item[href="/example"]': 1 +dropdown_link_without_wrapper: + render: + '#type': component + '#component': oe_bootstrap_theme:dropdown + '#props': + dropdown_id: link-dropdown + link: true + remove_wrapper: true + trigger: + label: 'Linked dropdown' + path: '/example' + attributes: + class: + - custom-link-trigger + items: + - label: 'A link' + path: '/target' + assertions: + count: + 'div.dropdown': 0 + 'a#link-dropdown.dropdown-toggle.custom-link-trigger[href="/example"][role="button"]': 1 + 'a#link-dropdown[autocomplete]': 0 + 'ul.dropdown-menu[aria-labelledby="link-dropdown"]': 1 + 'a.dropdown-item[href="/target"]': 1 +dropdown_inside_navigation_with_fallback_id: + render: + '#type': component + '#component': oe_bootstrap_theme:dropdown + '#props': + button_label: 'Navigation dropdown' + inside_navigation: true + items: + - label: 'A link' + path: '/example' + assertions: + count: + 'div.dropdown': 1 + 'button[id^="dropdown"].dropdown-toggle[data-bs-toggle="dropdown"]': 1 + 'button.dropdown-toggle[autocomplete]': 0 + 'ul.dropdown-menu[aria-labelledby^="dropdown"][aria-hidden="true"][role="presentation"]': 1 diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/fact_figures.yml b/tests/src/Kernel/fixtures/markup_rendering_components/fact_figures.yml new file mode 100644 index 000000000..fd032d82a --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/fact_figures.yml @@ -0,0 +1,162 @@ +fact_figures_default_1col: + render: + '#type': component + '#component': oe_bootstrap_theme:fact_figures + '#props': + columns: 1 + link: + label: 'Read more' + path: '/example' + title: 'Fact and figures block' + title_tag: 'h3' + items: + - icon: 'sticky' + subtitle: 'Jira Tickets' + title: '1529 JIRA Ticket' + description: 'Nunc condimentum sapien ut nibh finibus suscipit vitae at justo. Morbi quis odio faucibus, commodo tortor id, elementum liber' + - icon: 'inboxes' + subtitle: 'Feature tickets' + title: '337 Features' + description: 'Turpis varius congue venenatis, erat dui feugiat felis.' + - icon: 'shield-check' + subtitle: 'Test tickets' + title: '107 Tests' + description: 'Cras vestibulum efficitur mi, quis porta tellus rutrum ut. Quisque at pulvinar sem.' + - icon: 'file-earmark-code' + subtitle: 'Test variants' + title: '5670 Variants' + description: 'Aliquam lacinia diam eu sem malesuada, in interdum ante bibendum.' + - icon: 'inbox' + subtitle: 'Jira ticket' + title: '345 Dev Ticket' + description: 'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis nec lectus tortor.' + - icon: 'eye' + subtitle: 'Figma components' + title: '43 Components' + description: 'Sed efficitur bibendum rutrum. Nunc feugiat congue augue ac consectetur.' + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:fact_figures"].bcl-fact-figures.bcl-fact-figures--default': 1 + 'h3.bcl-heading': 1 + 'div.row.row-cols-md-1': 1 + 'div.row.row-cols-sm-2': 0 + 'svg.bi.icon--l': 6 + 'svg use': 6 + contains: + 'svg': '#sticky' + 'a[href="/example"]': 'Read more' + equals: + 'h3.bcl-heading': 'Fact and figures block' + 'div > div.col:nth-child(1) > div.fs-4': '1529 JIRA Ticket' + 'div > div.col:nth-child(2) > div.fs-4': '337 Features' + 'div > div.col:nth-child(3) > div.fs-4': '107 Tests' + 'div > div.col:nth-child(4) > div.fs-4': '5670 Variants' + 'div > div.col:nth-child(5) > div.fs-4': '345 Dev Ticket' + 'div > div.col:nth-child(6) > div.fs-4': '43 Components' + 'div > div.col:nth-child(1) > div.fs-5': 'Jira Tickets' + 'div > div.col:nth-child(2) > div.fs-5': 'Feature tickets' + 'div > div.col:nth-child(3) > div.fs-5': 'Test tickets' + 'div > div.col:nth-child(4) > div.fs-5': 'Test variants' + 'div > div.col:nth-child(5) > div.fs-5': 'Jira ticket' + 'div > div.col:nth-child(6) > div.fs-5': 'Figma components' + 'a[href="/example"]': 'Read more' +fact_figures_default_2col: + render: + '#type': component + '#component': oe_bootstrap_theme:fact_figures + '#props': + columns: 2 + title: 'Fact and figures block' + items: + - icon: 'sticky' + subtitle: 'Jira Tickets' + title: '1529 JIRA Ticket' + description: 'Nunc condimentum sapien ut nibh finibus suscipit vitae at justo. Morbi quis odio faucibus, commodo tortor id, elementum liber' + - icon: 'inboxes' + subtitle: 'Feature tickets' + title: '337 Features' + description: 'Turpis varius congue venenatis, erat dui feugiat felis.' + - icon: 'shield-check' + subtitle: 'Test tickets' + title: '107 Tests' + description: 'Cras vestibulum efficitur mi, quis porta tellus rutrum ut. Quisque at pulvinar sem.' + - icon: 'file-earmark-code' + subtitle: 'Test variants' + title: '5670 Variants' + description: 'Aliquam lacinia diam eu sem malesuada, in interdum ante bibendum.' + - icon: 'inbox' + subtitle: 'Jira ticket' + title: '345 Dev Ticket' + description: 'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis nec lectus tortor. ' + - icon: 'eye' + subtitle: 'Figma components' + title: '43 Components' + description: 'Sed efficitur bibendum rutrum. Nunc feugiat congue augue ac consectetur.' + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:fact_figures"].bcl-fact-figures.bcl-fact-figures--default': 1 + 'h2.bcl-heading': 1 + 'div.row.row-cols-md-2': 1 + 'div.row.row-cols-sm-2': 0 + 'svg.bi.icon--l': 6 + 'div.border-top': 0 + 'a': 0 + equals: + 'h2.bcl-heading': 'Fact and figures block' + 'div > div.col:nth-child(1) > div.fs-4': '1529 JIRA Ticket' + 'div > div.col:nth-child(6) > div.fs-4': '43 Components' +fact_figures_default_3col: + render: + '#type': component + '#component': oe_bootstrap_theme:fact_figures + '#props': + columns: 3 + link: + label: 'Read more' + path: '/example' + title: 'Fact and figures block' + items: + - icon: 'sticky' + subtitle: 'Jira Tickets' + title: '1529 JIRA Ticket' + description: 'Nunc condimentum sapien ut nibh finibus suscipit vitae at justo. Morbi quis odio faucibus, commodo tortor id, elementum liber' + - icon: 'inboxes' + subtitle: 'Feature tickets' + title: '337 Features' + description: 'Turpis varius congue venenatis, erat dui feugiat felis.' + - icon: 'shield-check' + subtitle: 'Test tickets' + title: '107 Tests' + description: 'Cras vestibulum efficitur mi, quis porta tellus rutrum ut. Quisque at pulvinar sem.' + - icon: 'file-earmark-code' + subtitle: 'Test variants' + title: '5670 Variants' + description: 'Aliquam lacinia diam eu sem malesuada, in interdum ante bibendum.' + - icon: + name: 'inbox' + attributes: + class: + - custom-class + subtitle: 'Jira ticket' + title: '345 Dev Ticket' + description: 'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis nec lectus tortor. ' + - icon: 'eye' + subtitle: 'Figma components' + title: '43 Components' + description: 'Sed efficitur bibendum rutrum. Nunc feugiat congue augue ac consectetur.' + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:fact_figures"].bcl-fact-figures.bcl-fact-figures--default': 1 + 'h2.bcl-heading': 1 + 'div.row.row-cols-md-3': 1 + 'div.row.row-cols-sm-2': 1 + 'svg.bi.icon--l': 6 + 'svg.custom-class.mb-2.icon--primary': 1 + contains: + 'div > div.col:nth-child(5) > svg': '#inbox' + 'a[href="/example"]': 'Read more' + equals: + 'h2.bcl-heading': 'Fact and figures block' + 'div > div.col:nth-child(1) > div.fs-4': '1529 JIRA Ticket' + 'div > div.col:nth-child(6) > div.fs-4': '43 Components' + 'a[href="/example"]': 'Read more' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/featured_media.yml b/tests/src/Kernel/fixtures/markup_rendering_components/featured_media.yml new file mode 100644 index 000000000..51f5715b3 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/featured_media.yml @@ -0,0 +1,241 @@ +featured_media_image_without_text: + render: + '#type': component + '#component': oe_bootstrap_theme:featured_media + '#props': + image: + src: 'https://picsum.photos/1200/600/' + alt: 'Alternative text for featured image' + title: 'Featured image title' + loading: lazy + width: 1200 + height: 600 + class: + - custom-image + caption: 'Media description text goes here.' + copyright: '© 2026 Example Organisation' + attributes: + id: featured-media-image + class: + - custom-featured-media + media_attributes: + class: + - custom-media + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:featured_media"]#featured-media-image.bcl-featured-media.custom-featured-media': 1 + 'div.bcl-featured-media > div.row': 1 + 'div.bcl-featured-media > div.row > div.col-12.col-md-4': 1 + 'div.bcl-featured-media > div.row > div.col-12.col-md-4 > figure.rounded.overflow-hidden.custom-media': 1 + 'figure > img.img-fluid.flex-end-100.custom-image[src="https://picsum.photos/1200/600/"][alt="Alternative text for featured image"][title="Featured image title"][loading="lazy"][width="1200"][height="600"]': 1 + 'figure > figcaption > div.bg-light.p-3': 1 + 'figure > figcaption > div.bcl-copyright': 1 + 'h2': 0 + 'a': 0 + 'iframe': 0 + 'video': 0 + equals: + 'figcaption > div.bg-light.p-3': 'Media description text goes here.' + '.bcl-copyright > span.visually-hidden': 'Image credit:' + contains: + '.bcl-copyright': '© 2026 Example Organisation' + +featured_media_image_with_text_and_link: + render: + '#type': component + '#component': oe_bootstrap_theme:featured_media + '#props': + image: + src: 'https://picsum.photos/1200/600/' + alt: 'Alternative text for featured image' + name: 'Image value object title' + responsive: false + title: 'Linked featured media title' + title_tag: h3 + title_link: + label: 'Linked featured media title' + path: '/featured' + id: featured-title-link + disabled: true + icon: + name: link-45deg + transformation: flip-horizontal + attributes: + class: + - custom-title-icon + icon_position: before + icon_display: inline-flex + remove_icon_spacers: true + attributes: + class: + - custom-title-link + title_attributes: + id: featured-title + class: + - custom-title + subtitle: 'Featured media subtitle' + subtitle_tag: h4 + subtitle_attributes: + id: featured-subtitle + class: + - custom-subtitle + text: + '#markup': '' + link: + label: 'Read more' + path: '/example' + variant: primary + id: featured-link + disabled: false + standalone: true + icon: + name: arrow-right + transformation: rotate-90 + attributes: + class: + - custom-icon + icon_position: before + icon_display: inline-flex + remove_icon_spacers: false + attributes: + class: + - custom-link + caption: 'Custom caption.' + content_classes: 'custom-caption' + icon_path: '/custom-icons.svg' + assertions: + count: + 'div.bcl-featured-media > h3#featured-title.bcl-heading.mb-4.custom-title': 1 + 'h3#featured-title > a#featured-title-link[href="/featured"][aria-disabled="true"][tabindex="-1"].disabled.standalone.custom-title-link': 1 + 'a#featured-title-link.gap-2-5': 0 + 'a#featured-title-link.d-inline-flex': 0 + 'a#featured-title-link > svg.custom-title-icon.icon--flip-horizontal': 1 + 'div.bcl-featured-media > div.row > div.col-12.col-md-6.order-1': 1 + 'div.bcl-featured-media > div.row > div.col-12.col-md-6.order-2': 1 + 'div.col-12.col-md-6.order-1 > h4#featured-subtitle.text-secondary.custom-subtitle': 1 + 'div.col-12.col-md-6.order-1 p.featured-description': 1 + 'a#featured-link[href="/example"].link-primary.standalone.d-inline-flex.gap-2-5.custom-link': 1 + 'a#featured-link svg.bi.icon--s.icon--rotate-90.custom-icon': 1 + 'figure > img.flex-end-100[title="Image value object title"]': 1 + 'figure > img.img-fluid': 0 + 'figure > figcaption > div.custom-caption': 1 + 'figure .bcl-copyright': 0 + equals: + 'h4#featured-subtitle': 'Featured media subtitle' + 'p.featured-description': 'Featured media description.' + 'figcaption > div.custom-caption': 'Custom caption.' + contains: + 'h3#featured-title > a': 'Linked featured media title' + 'a#featured-title-link svg': '/custom-icons.svg#link-45deg' + 'a#featured-link': 'Read more' + 'a#featured-link svg': '/custom-icons.svg#arrow-right' + +featured_media_embedded_right_custom_ratio: + render: + '#type': component + '#component': oe_bootstrap_theme:featured_media + '#props': + embedded_media: + '#type': html_tag + '#tag': iframe + '#attributes': + src: 'https://example.com/embed' + title: 'Embedded media' + ratio: 21x9 + title: 'Embedded featured media' + subtitle: 'Supporting subtitle' + text: 'Text displayed on the right.' + text_position: right + caption: 'Embedded media caption.' + copyright: '© 2026 Example Organisation' + copyright_label: 'Media copyright:' + assertions: + count: + 'div.bcl-featured-media > h2.bcl-heading.mb-4': 1 + 'div.bcl-featured-media > div.row > div.col-12.col-md-6.order-1 > figure': 1 + 'div.bcl-featured-media > div.row > div.col-12.col-md-6.order-2': 1 + 'div.col-12.col-md-6.order-2 > h3.text-secondary': 1 + 'figure > div.ratio.ratio-21x9 > iframe[src="https://example.com/embed"][title="Embedded media"]': 1 + 'figure > img': 0 + 'figure > video': 0 + equals: + 'h2.bcl-heading': 'Embedded featured media' + 'h3.text-secondary': 'Supporting subtitle' + 'figcaption > div.bg-light.p-3': 'Embedded media caption.' + '.bcl-copyright > span.visually-hidden': 'Media copyright:' + contains: + 'div.col-12.col-md-6.order-2': 'Text displayed on the right.' + '.bcl-copyright': '© 2026 Example Organisation' + +featured_media_embedded_defaults_without_title: + render: + '#type': component + '#component': oe_bootstrap_theme:featured_media + '#props': + embedded_media: + '#type': html_tag + '#tag': iframe + '#attributes': + src: 'https://example.com/default-ratio' + title: 'Default ratio' + subtitle: 'Subtitle without a title' + assertions: + count: + 'h2.bcl-heading': 0 + 'h2.text-secondary': 1 + 'div.bcl-featured-media > div.row > div.col-12.col-md-6.order-1': 1 + 'div.bcl-featured-media > div.row > div.col-12.col-md-6.order-2 > figure': 1 + 'figure > div.ratio.ratio-16x9 > iframe[src="https://example.com/default-ratio"]': 1 + 'figure > figcaption': 0 + 'a': 0 + equals: + 'h2.text-secondary': 'Subtitle without a title' + +featured_media_native_video: + render: + '#type': component + '#component': oe_bootstrap_theme:featured_media + '#props': + poster_image: 'https://picsum.photos/1200/675/' + sources: + - type: video/mp4 + src: 'https://example.com/video.mp4' + - type: video/webm + src: 'https://example.com/video.webm' + tracks: + - kind: captions + label: English + src: 'https://example.com/captions-en.vtt' + src_lang: en + - kind: subtitles + label: Français + src: 'https://example.com/subtitles-fr.vtt' + src_lang: fr + caption: 'Native video caption.' + assertions: + count: + 'div.bcl-featured-media > div.row > div.col-12.col-md-4 > figure': 1 + 'figure > video[controls][poster="https://picsum.photos/1200/675/"].w-100.d-block': 1 + 'video > source': 2 + 'video > source[type="video/mp4"][src="https://example.com/video.mp4"]': 1 + 'video > source[type="video/webm"][src="https://example.com/video.webm"]': 1 + 'video > track': 2 + 'video > track[kind="captions"][label="English"][src="https://example.com/captions-en.vtt"][srclang="en"]': 1 + 'video > track[kind="subtitles"][label="Français"][src="https://example.com/subtitles-fr.vtt"][srclang="fr"]': 1 + 'figure > img': 0 + 'figure iframe': 0 + equals: + 'figcaption > div.bg-light.p-3': 'Native video caption.' + +featured_media_image_as_markup: + render: + '#type': component + '#component': oe_bootstrap_theme:featured_media + '#props': + image: + '#markup': 'Pre-rendered image' + assertions: + count: + 'figure > img[src="https://example.com/pre-rendered.jpg"][alt="Pre-rendered image"]': 1 + 'figure > img.img-fluid': 0 + 'figure > img.flex-end-100': 0 diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/file.yml b/tests/src/Kernel/fixtures/markup_rendering_components/file.yml new file mode 100644 index 000000000..2ef9e8a3f --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/file.yml @@ -0,0 +1,132 @@ +file_default_without_translations: + render: + '#type': component + '#component': oe_bootstrap_theme:file + '#props': + title: 'Download the document' + file: + title: 'Example document' + url: 'https://example.com/document.pdf' + size: 4444 + extension: pdf + language_code: en + assertions: + count: + 'div.bcl-file-container.mt-4 > h2.mb-4.bcl-heading': 1 + 'div.bcl-file-container > div.bcl-file.border.rounded': 1 + '.pe-2.pb-2 > svg': 1 + 'a[download][target="_blank"][href="https://example.com/document.pdf"]': 1 + '.bcl-file .collapse': 0 + equals: + 'h2.bcl-heading': 'Download the document' + 'p.fw-medium.fs-5': 'Example document' + contains: + '.bcl-file small.fw-medium': 'English' + '.bcl-file small.fw-medium span.fw-normal': '(4.34 KB - PDF)' + '.pe-2.pb-2 > svg': '/icons/bcl-default-icons.svg#file-pdf-fill' + 'a[download] svg': '/icons/bcl-default-icons.svg#download' + +file_with_translations_and_custom_link_label: + render: + '#type': component + '#component': oe_bootstrap_theme:file + '#props': + link_label: 'Get the file' + file: + title: 'Example document' + url: 'https://example.com/document.pdf' + extension: pdf + language_code: en + translations: + - title: 'Exemple de document' + url: 'https://example.com/document-fr.pdf' + size: 4321 + extension: pdf + language_code: fr + - title: 'Пример документ' + url: 'https://example.com/document-bg.docx' + size: 40391 + extension: docx + language_code: bg + meta: 'Draft' + assertions: + count: + 'h2': 0 + '.bcl-file > .border-top': 1 + '.bcl-file .collapse .row': 2 + '.bcl-file .collapse a[href="https://example.com/document-fr.pdf"]': 1 + '.bcl-file .collapse a[href="https://example.com/document-bg.docx"]': 1 + equals: + '.collapse > div.py-3.border-bottom p.fw-bold': 'Exemple de document' + '.collapse > div.pt-3 p.fw-bold': 'Пример документ' + '.collapse > div.pt-3 small.fw-bold span.fw-normal': 'Draft' + contains: + 'a[href="https://example.com/document.pdf"]': 'Get the file' + '.text-end.text-md-center a': 'Other languages (2)' + '.collapse > div.py-3.border-bottom small.fw-bold': 'français' + '.collapse > div.py-3.border-bottom small.fw-bold span.fw-normal': '(4.22 KB - PDF)' + '.bcl-file .collapse a[href="https://example.com/document-fr.pdf"]': 'Get the file' + +file_custom_attributes_and_icon_path: + render: + '#type': component + '#component': oe_bootstrap_theme:file + '#props': + title: 'Custom attrs file' + title_attributes: + class: + - custom-title-class + attributes: + id: custom-file + class: + - custom-file-class + data-file: custom-value + icon_path: '/custom-icons.svg' + file: + title: 'Example document' + url: 'https://example.com/document.pdf' + size: 4444 + extension: pdf + language_code: en + meta: 'Updated last week' + assertions: + count: + 'h2.custom-title-class.bcl-heading': 1 + 'h2.mb-4': 0 + '#custom-file.bcl-file.custom-file-class': 1 + '#custom-file[data-file="custom-value"]': 1 + equals: + '.bcl-file small.fw-medium span.fw-normal': 'Updated last week' + contains: + '.pe-2.pb-2 > svg': '/custom-icons.svg#file-pdf-fill' + 'a[download] svg': '/custom-icons.svg#download' + +file_without_language_code_or_size: + render: + '#type': component + '#component': oe_bootstrap_theme:file + '#props': + file: + title: 'No language file' + url: 'https://example.com/no-lang.txt' + extension: txt + assertions: + count: + 'h2': 0 + '.bcl-file small.fw-medium': 1 + equals: + '.bcl-file small.fw-medium': ' (TXT)' + contains: + '.pe-2.pb-2 > svg': '/icons/bcl-default-icons.svg#file-text-fill' + +file_without_file_renders_nothing: + render: + '#type': component + '#component': oe_bootstrap_theme:file + '#props': + title: 'Should not render body' + assertions: + count: + 'h2': 0 + '.bcl-file-container': 0 + '.bcl-file': 0 diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/gallery.yml b/tests/src/Kernel/fixtures/markup_rendering_components/gallery.yml new file mode 100644 index 000000000..3558592b7 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/gallery.yml @@ -0,0 +1,237 @@ +gallery_mixed_media: + render: + '#type': component + '#component': oe_bootstrap_theme:gallery + '#props': + items: + - media: + '#markup': 'First media image' + thumbnail: + '#markup': 'First thumbnail' + caption_title: 'First image' + caption: 'First image caption.' + - media: + '#markup': '' + '#allowed_tags': + - video + - source + thumbnail: + '#markup': 'Video thumbnail' + caption_title: 'Featured video' + caption: 'Video caption.' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\GalleryPatternAssert + expected: + title: ~ + items: + - thumbnail: + rendered: 'First thumbnail' + caption_title: 'First image' + caption: 'First image caption.' + media: + rendered: 'First media image' + caption_title: 'First image' + caption: 'First image caption.' + - thumbnail: + rendered: 'Video thumbnail' + caption_title: 'Featured video' + caption: 'Video caption.' + play_icon: true + media: + rendered: '' + caption_title: ~ + caption: ~ + +gallery_default_labels_and_limit: + render: + '#type': component + '#component': oe_bootstrap_theme:gallery + '#props': + title: 'Default gallery title' + counter: '%d items' + items: + - media: + '#markup': 'Media 1' + thumbnail: + '#markup': 'Thumbnail 1' + - media: + '#markup': 'Media 2' + thumbnail: + '#markup': 'Thumbnail 2' + - media: + '#markup': 'Media 3' + thumbnail: + '#markup': 'Thumbnail 3' + - media: + '#markup': 'Media 4' + thumbnail: + '#markup': 'Thumbnail 4' + - media: + '#markup': 'Media 5' + thumbnail: + '#markup': 'Thumbnail 5' + - media: + '#markup': 'Media 6' + thumbnail: + '#markup': 'Thumbnail 6' + assertions: + count: + '.bcl-gallery': 1 + '.bcl-gallery > .d-sm-flex h2.bcl-heading': 1 + '.bcl-gallery__thumbnails > ul.bcl-gallery__grid > li': 5 + '.bcl-gallery__thumbnails-collaspe > ul.bcl-gallery__grid > li': 1 + '.bcl-gallery__collapse': 1 + '.bcl-gallery__mobile-view-more': 1 + '.modal .carousel-item': 6 + equals: + '.bcl-gallery > .d-sm-flex .fw-bold': '6 items' + '.bcl-gallery__collapse .label-collapsed': 'View the full gallery (6)' + '.bcl-gallery__collapse .label-expanded': 'View less' + '.modal .carousel-control-prev .visually-hidden': 'Previous' + '.modal .carousel-control-next .visually-hidden': 'Next' + +gallery_with_all_parameters: + render: + '#type': component + '#component': oe_bootstrap_theme:gallery + '#props': + gallery_id: custom-gallery + title: 'Linked gallery title' + title_tag: h4 + title_link: + path: 'https://example.com/gallery' + variant: danger + id: custom-gallery-title-link + disabled: true + icon: + name: images + size: l + transformation: flip-horizontal + attributes: + class: + - custom-title-icon + icon_position: before + icon_display: inline-flex + remove_icon_spacers: true + attributes: + class: + - custom-title-link + title_attributes: + id: custom-gallery-title + class: + - custom-title-class + counter: 'Media count: %d' + collapse_button_label: 'Show all %d media items' + expand_button_label: 'Show fewer media items' + previous_label: 'Previous media' + next_label: 'Next media' + max_visible_thumbnails: 2 + icon_path: '/custom-icons.svg' + attributes: + id: custom-gallery-wrapper + class: + - custom-gallery-class + data-gallery-attribute: custom-value + items: + - media: + '#markup': 'Custom media 1' + thumbnail: + '#markup': 'Custom thumbnail 1' + caption_title: 'Custom image' + caption: 'Custom image caption.' + copyright: '© 2026 Example Organisation' + copyright_label: 'Custom credit:' + - media: + '#markup': '' + '#allowed_tags': + - iframe + thumbnail: + '#markup': 'Embedded media thumbnail' + caption_title: 'Embedded media' + caption: 'Embedded media caption.' + copyright: '© 2026 Video Organisation' + - media: + '#markup': 'Custom media 3' + thumbnail: + '#markup': 'Custom thumbnail 3' + caption_title: 'Linked image' + caption: 'Linked image caption.' + caption_classes: custom-caption-class + interval: 3000 + link: + label: 'Read more' + path: 'https://example.com/read-more' + variant: primary + id: custom-gallery-link + disabled: true + standalone: true + icon: + name: chevron-right + size: xs + transformation: rotate-90 + attributes: + class: + - custom-link-icon + icon_position: after + icon_display: flex + remove_icon_spacers: false + attributes: + class: + - custom-gallery-link + assertions: + count: + '#custom-gallery-wrapper.bcl-gallery.custom-gallery-class[data-gallery-attribute="custom-value"]': 1 + 'h4#custom-gallery-title.bcl-heading.custom-title-class': 1 + 'h4#custom-gallery-title > a#custom-gallery-title-link.custom-title-link.link-danger.disabled.standalone[href="https://example.com/gallery"][aria-disabled="true"][tabindex="-1"]': 1 + '#custom-gallery-title-link.d-inline-flex': 0 + '#custom-gallery-title-link.gap-2-5': 0 + 'h4#custom-gallery-title svg.custom-title-icon.icon--l.icon--flip-horizontal': 1 + '.bcl-gallery__thumbnails > ul.bcl-gallery__grid > li': 2 + '.bcl-gallery__thumbnails-collaspe > ul.bcl-gallery__grid > li': 1 + '#modalCustom-gallery.modal': 1 + '#carouselCustom-gallery.carousel': 1 + '#carouselCustom-gallery .carousel-item': 3 + '#carouselCustom-gallery .carousel-item:nth-child(2) .carousel-caption': 0 + '#carouselCustom-gallery .carousel-item:nth-child(3)[data-bs-interval="3000"]': 1 + '#custom-gallery-link.custom-gallery-link.link-primary.disabled.standalone.d-flex.gap-2-5[href="https://example.com/read-more"][aria-disabled="true"][tabindex="-1"]': 1 + '#custom-gallery-link svg.custom-link-icon.icon--xs.icon--rotate-90': 1 + equals: + '.bcl-gallery > .d-sm-flex .fw-bold': 'Media count: 3' + '.bcl-gallery__collapse .label-collapsed': 'Show all 3 media items' + '.bcl-gallery__collapse .label-expanded': 'Show fewer media items' + '#carouselCustom-gallery .carousel-control-prev .visually-hidden': 'Previous media' + '#carouselCustom-gallery .carousel-control-next .visually-hidden': 'Next media' + '#carouselCustom-gallery .carousel-item:nth-child(1) .bcl-copyright': 'Custom credit: © 2026 Example Organisation' + '#carouselCustom-gallery .carousel-item:nth-child(2) .bcl-copyright': 'Copyright: © 2026 Video Organisation' + contains: + '#custom-gallery-title > a': 'Linked gallery title' + 'h4#custom-gallery-title svg': '/custom-icons.svg#images' + '.bcl-gallery__item-play-icon svg': '/custom-icons.svg#play-fill' + '#custom-gallery-link svg': '/custom-icons.svg#chevron-right' + +gallery_with_fallback_id: + render: + '#type': component + '#component': oe_bootstrap_theme:gallery + '#props': + items: + - media: + '#markup': 'Media' + thumbnail: + '#markup': 'Thumbnail' + assertions: + count: + '.bcl-gallery .modal[id^="modalGallery"]': 1 + '.bcl-gallery .carousel[id^="carouselGallery"]': 1 + '.bcl-gallery__item[data-bs-target^="#carouselGallery"]': 1 + '.bcl-gallery__item-overlay[data-bs-target^="#modalGallery"]': 1 + +gallery_without_items: + render: + '#type': component + '#component': oe_bootstrap_theme:gallery + '#props': {} + assertions: + count: + '.bcl-gallery': 0 diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/icon.yml b/tests/src/Kernel/fixtures/markup_rendering_components/icon.yml new file mode 100644 index 000000000..98aaac894 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/icon.yml @@ -0,0 +1,89 @@ +icon_default: + render: + '#type': component + '#component': oe_bootstrap_theme:icon + '#props': {} + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\IconPatternAssert + expected: + name: '' + size: s + +icon_with_name_and_default_size: + render: + '#type': component + '#component': oe_bootstrap_theme:icon + '#props': + name: envelope + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\IconPatternAssert + expected: + name: envelope + size: s + +icon_with_smallest_size: + render: + '#type': component + '#component': oe_bootstrap_theme:icon + '#props': + name: eye + size: 2xs + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\IconPatternAssert + expected: + name: eye + size: 2xs + +icon_with_fluid_size: + render: + '#type': component + '#component': oe_bootstrap_theme:icon + '#props': + name: eye + size: fluid + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\IconPatternAssert + expected: + name: eye + size: fluid + +icon_with_transformation_and_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:icon + '#props': + name: arrow-right + size: xl + transformation: rotate-90 + attributes: + id: custom-icon + class: + - custom-icon-class + data-icon-attribute: custom-value + role: img + aria-label: 'Next item' + assertions: + count: + 'svg#custom-icon.bi.icon--xl.icon--rotate-90.custom-icon-class[data-icon-attribute="custom-value"][role="img"][aria-label="Next item"]': 1 + 'svg.icon--rotate-180': 0 + contains: + 'svg#custom-icon': 'assets/icons/bcl-default-icons.svg#arrow-right' + +icon_with_custom_path: + render: + '#type': component + '#component': oe_bootstrap_theme:icon + '#props': + name: custom-symbol + size: m + transformation: flip-vertical + icon_path: '/custom/icons.svg' + assertions: + count: + 'svg.bi.icon--m.icon--flip-vertical': 1 + contains: + 'svg': '/custom/icons.svg#custom-symbol' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/inpage_navigation.yml b/tests/src/Kernel/fixtures/markup_rendering_components/inpage_navigation.yml new file mode 100644 index 000000000..4bf0e3678 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/inpage_navigation.yml @@ -0,0 +1,173 @@ +inpage_navigation_default_props_omitted: + render: + '#type': component + '#component': oe_bootstrap_theme:inpage_navigation + '#props': {} + assertions: + count: + 'nav[data-component-id="oe_bootstrap_theme:inpage_navigation"]': 1 + 'div.row > div.bcl-sidebar.col-md-3': 1 + 'div.row > div.col-md-9': 1 + 'nav.bcl-inpage-navigation.position-sticky[id^="bcl-inpage-navigation"]': 1 + 'nav.dynamic-active': 0 + 'button.dropdown-toggle.h2.d-md-none[id^="bcl-inpage-navigation"][aria-expanded="false"][data-bs-toggle="dropdown"]': 1 + 'button.dropdown-toggle svg.icon--s': 1 + 'h2.bcl-heading': 0 + 'ul.nav.nav-pills.flex-column': 1 + 'ul.nav-pills > li': 0 + equals: + 'div.row > div.col-md-9': '' + +inpage_navigation_with_content_and_links: + render: + '#type': component + '#component': oe_bootstrap_theme:inpage_navigation + '#props': + title: 'Page content' + id: page-navigation + dropdown_id: page-navigation-dropdown + links: + - label: + '#markup': 'Heading one' + path: '#heading-1' + active: true + attributes: + class: + - custom-navigation-link + data-link-attribute: first + - label: 'Heading two' + path: '#heading-2' + id: heading-two-link + variant: warning + standalone: true + - label: 'Unavailable heading' + path: '#heading-3' + disabled: true + content: + '#markup': '

Heading one

First section content.

Heading two

Second section content.

' + assertions: + count: + 'nav#page-navigation.bcl-inpage-navigation[aria-labelledby="page-navigation-dropdown"]': 1 + 'button#page-navigation-dropdown.dropdown-toggle[type="button"]': 1 + 'button#page-navigation-dropdown svg.icon--s': 1 + 'nav#page-navigation h2.bcl-heading': 1 + 'ul.nav-pills > li.nav-item': 3 + 'a.nav-link.active.custom-navigation-link[href="#heading-1"][data-link-attribute="first"]': 1 + 'a#heading-two-link.nav-link.link-warning.standalone[href="#heading-2"]': 1 + 'a.nav-link.disabled[href="#heading-3"][aria-disabled="true"][tabindex="-1"]': 1 + 'div.col-md-9 > h2#heading-1': 1 + 'div.col-md-9 > h2#heading-2': 1 + equals: + 'nav#page-navigation h2.bcl-heading': 'Page content' + 'a.custom-navigation-link strong': one + 'div.col-md-9 > h2#heading-2': 'Heading two' + contains: + 'button#page-navigation-dropdown svg': 'assets/icons/bcl-default-icons.svg#chevron-down' + +inpage_navigation_with_all_optional_parameters: + render: + '#type': component + '#component': oe_bootstrap_theme:inpage_navigation + '#props': + title: + '#markup': 'Related information' + title_tag: h3 + title_link: + path: '/overview' + variant: success + id: linked-navigation-title + disabled: true + icon: + name: arrow-right + size: l + transformation: rotate-90 + attributes: + class: + - custom-title-icon + icon_position: after + icon_display: inline-flex + attributes: + class: + - custom-title-link + data-title-link-attribute: custom-value + title_attributes: + class: + - custom-title + data-title-attribute: custom-value + links: + - label: 'Active section' + path: '#active-section' + active: true + icon: + name: bookmark + attributes: + class: + - custom-link-icon + icon_position: before + icon_display: inline-flex + attributes: + class: + - custom-link + - label: 'Custom sprite section' + path: '#custom-section' + variant: danger + icon: + name: custom-symbol + path: '/other/icons.svg' + size: xl + transformation: flip-vertical + attributes: + class: + - custom-sprite-icon + remove_icon_spacers: true + content: + '#markup': '

Active section

Active content.

Custom section

Custom content.

' + dynamic_active: true + icon_path: '/custom/icons.svg' + attributes: + id: attributes-navigation + class: + - custom-navigation + aria-label: 'Related information' + data-navigation-attribute: custom-value + assertions: + count: + 'nav#attributes-navigation.bcl-inpage-navigation.position-sticky.dynamic-active.custom-navigation[aria-label="Related information"][data-navigation-attribute="custom-value"]': 1 + 'nav#attributes-navigation[aria-labelledby]': 0 + 'button#attributes-navigation-dropdown.dropdown-toggle': 1 + 'nav h3.bcl-heading.custom-title[data-title-attribute="custom-value"]': 1 + 'h3.bcl-heading > a#linked-navigation-title.link-success.standalone.disabled.d-inline-flex.gap-2-5.custom-title-link[href="/overview"][aria-disabled="true"][tabindex="-1"][data-title-link-attribute="custom-value"]': 1 + 'h3.bcl-heading a > svg.icon--l.icon--rotate-90.custom-title-icon': 1 + 'a.nav-link.active.custom-link.d-inline-flex.gap-2-5[href="#active-section"]': 1 + 'a.custom-link > svg:first-child.custom-link-icon': 1 + 'a.nav-link.link-danger[href="#custom-section"] > svg.icon--xl.icon--flip-vertical.custom-sprite-icon': 1 + 'a[href="#custom-section"] > svg.ms-2-5': 0 + 'a[href="#custom-section"] > svg.me-2-5': 0 + equals: + 'h3.bcl-heading a em': information + contains: + 'button#attributes-navigation-dropdown svg': '/custom/icons.svg#chevron-down' + 'h3.bcl-heading a svg': '/custom/icons.svg#arrow-right' + 'a.custom-link svg': '/custom/icons.svg#bookmark' + 'a[href="#custom-section"] svg': '/other/icons.svg#custom-symbol' + +inpage_navigation_without_links: + render: + '#type': component + '#component': oe_bootstrap_theme:inpage_navigation + '#props': + title: 'Page content' + id: navigation-without-links + dropdown_id: navigation-without-links-dropdown + content: + '#markup': '
Content remains visible without navigation links.
' + assertions: + count: + 'nav#navigation-without-links': 1 + 'button#navigation-without-links-dropdown': 1 + 'nav h2.bcl-heading': 1 + 'ul.nav-pills a.nav-link': 0 + 'div.col-md-9 > div.content-example': 1 + equals: + 'nav h2.bcl-heading': 'Page content' + 'div.content-example': 'Content remains visible without navigation links.' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/link.yml b/tests/src/Kernel/fixtures/markup_rendering_components/link.yml new file mode 100644 index 000000000..2670c6746 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/link.yml @@ -0,0 +1,158 @@ +link_default: + render: + '#type': component + '#component': oe_bootstrap_theme:link + '#props': {} + assertions: + count: + 'a[href="#"]': 1 + 'a.disabled': 0 + 'a.standalone': 0 + 'a[class*="link-"]': 0 + 'a svg': 0 + equals: + 'a': '' + +link_with_variant_id_and_render_array_label: + render: + '#type': component + '#component': oe_bootstrap_theme:link + '#props': + label: + '#markup': 'Read more' + path: 'https://example.com?type=link#details' + variant: warning + id: variant-link + assertions: + count: + 'a#variant-link.link-warning[href="https://example.com?type=link#details"]': 1 + 'a#variant-link strong': 1 + equals: + 'a#variant-link strong': more + +link_disabled_standalone_with_custom_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:link + '#props': + label: 'Disabled standalone link' + path: '/example' + disabled: true + standalone: true + attributes: + class: + - custom-link-class + data-link-attribute: custom-value + title: 'Custom link title' + assertions: + count: + 'a.disabled.standalone.custom-link-class[href="/example"][aria-disabled="true"][tabindex="-1"]': 1 + 'a[data-link-attribute="custom-value"][title="Custom link title"]': 1 + equals: + 'a': 'Disabled standalone link' + +link_with_string_icon_before: + render: + '#type': component + '#component': oe_bootstrap_theme:link + '#props': + label: 'Previous page' + path: '/previous' + icon: arrow-left + icon_position: before + assertions: + count: + 'a[href="/previous"] > svg:first-child.bi.icon--s.me-2-5': 1 + 'a > svg.ms-2-5': 0 + contains: + 'a > svg': 'assets/icons/bcl-default-icons.svg#arrow-left' + +link_with_icon_object_after: + render: + '#type': component + '#component': oe_bootstrap_theme:link + '#props': + label: 'Custom icon' + path: '/custom-icon' + icon: + name: custom-symbol + path: '/custom/icons.svg' + size: xl + transformation: rotate-90 + attributes: + class: + - custom-icon-class + data-icon-attribute: custom-value + icon_position: after + assertions: + count: + 'a[href="/custom-icon"] > svg:last-child.bi.icon--xl.icon--rotate-90.ms-2-5.custom-icon-class[data-icon-attribute="custom-value"]': 1 + 'a > svg.me-2-5': 0 + contains: + 'a > svg': '/custom/icons.svg#custom-symbol' + +link_with_inline_flex_icon_display: + render: + '#type': component + '#component': oe_bootstrap_theme:link + '#props': + label: 'Inline flex link' + path: '/inline-flex' + icon: box-arrow-up-right + icon_display: inline-flex + assertions: + count: + 'a.d-inline-flex.gap-2-5': 1 + 'a > svg:last-child': 1 + 'a > svg.ms-2-5': 0 + 'a > svg.me-2-5': 0 + +link_with_flex_icon_display: + render: + '#type': component + '#component': oe_bootstrap_theme:link + '#props': + label: 'Flex link' + path: '/flex' + icon: download + icon_position: before + icon_display: flex + assertions: + count: + 'a.d-flex.gap-2-5': 1 + 'a > svg.ms-2-5': 0 + 'a > svg.me-2-5': 0 + +link_with_icon_only: + render: + '#type': component + '#component': oe_bootstrap_theme:link + '#props': + path: '/search' + icon: search + attributes: + aria-label: Search + assertions: + count: + 'a[href="/search"][aria-label="Search"] > svg': 1 + 'a > svg.ms-2-5': 0 + 'a > svg.me-2-5': 0 + contains: + 'a > svg': 'assets/icons/bcl-default-icons.svg#search' + +link_without_icon_spacers: + render: + '#type': component + '#component': oe_bootstrap_theme:link + '#props': + label: 'Link without icon spacing' + path: '/no-spacing' + icon: arrow-right + icon_display: inline-flex + remove_icon_spacers: true + assertions: + count: + 'a > svg': 1 + 'a.gap-2-5': 0 + 'a > svg.ms-2-5': 0 + 'a > svg.me-2-5': 0 diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/links_block.yml b/tests/src/Kernel/fixtures/markup_rendering_components/links_block.yml new file mode 100644 index 000000000..5aad275d4 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/links_block.yml @@ -0,0 +1,148 @@ +links_block_default_props_omitted: + render: + '#type': component + '#component': oe_bootstrap_theme:links_block + '#props': {} + assertions: + count: + '.bcl-links-block': 0 + +links_block_vertical_gray: + render: + '#type': component + '#component': oe_bootstrap_theme:links_block + '#props': + title: + '#markup': 'Useful resources' + links: + - label: + '#markup': 'Funding opportunities' + path: 'https://example.com/funding' + - label: 'Programme guidance' + path: 'https://example.com/guidance' + - label: 'Contact support' + path: 'https://example.com/support' + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:links_block"].bcl-links-block.bg-light.px-4.py-3.rounded-2': 1 + 'h2.bcl-heading.pb-3.mb-3.border-bottom': 1 + 'ul.ps-0.mb-0': 1 + 'ul > li.list-unstyled': 3 + 'ul > li.d-inline': 0 + 'ul > li.me-4-5': 2 + 'a.mb-3.d-inline-block.standalone': 3 + 'a svg': 0 + equals: + 'h2.bcl-heading strong': resources + 'li:nth-child(1) a em': opportunities + 'li:nth-child(2) a[href="https://example.com/guidance"]': 'Programme guidance' + +links_block_horizontal_transparent_with_optional_parameters: + render: + '#type': component + '#component': oe_bootstrap_theme:links_block + '#props': + title: + '#markup': 'Related information' + title_tag: h3 + title_link: + path: '/resources' + variant: success + id: resources-title-link + disabled: true + icon: + name: arrow-right + size: l + transformation: rotate-90 + attributes: + class: + - custom-title-icon + icon_position: after + icon_display: inline-flex + attributes: + class: + - custom-title-link + data-title-link-attribute: custom-value + title_attributes: + class: + - custom-title + data-title-attribute: custom-value + background: transparent + orientation: horizontal + links: + - label: + '#markup': 'Disabled link' + path: '/disabled' + variant: warning + id: disabled-link + disabled: true + icon: lock + icon_position: before + attributes: + class: + - custom-link + data-link-attribute: custom-value + - label: 'Custom icon' + path: '/custom-icon' + icon: + name: custom-symbol + path: '/other/icons.svg' + size: xl + transformation: flip-vertical + attributes: + class: + - custom-link-icon + icon_position: after + icon_display: inline-flex + - label: 'No icon spacing' + path: '/no-spacing' + icon: arrow-right + icon_display: inline-flex + remove_icon_spacers: true + icon_path: '/custom/icons.svg' + attributes: + id: custom-links-block + class: + - custom-links-block + data-block-attribute: custom-value + assertions: + count: + 'div#custom-links-block.bcl-links-block.custom-links-block[data-block-attribute="custom-value"]': 1 + 'div#custom-links-block.bg-light': 0 + 'h3.bcl-heading.pb-3.mb-3.border-bottom.custom-title[data-title-attribute="custom-value"]': 1 + 'h3 > a#resources-title-link.link-success.standalone.disabled.d-inline-flex.gap-2-5.custom-title-link[href="/resources"][aria-disabled="true"][tabindex="-1"][data-title-link-attribute="custom-value"]': 1 + 'h3 a > svg.icon--l.icon--rotate-90.custom-title-icon': 1 + 'ul > li.list-unstyled.d-inline': 3 + 'ul > li.me-4-5': 2 + 'a#disabled-link.link-warning.disabled.d-flex.gap-2-5.custom-link[href="/disabled"][aria-disabled="true"][tabindex="-1"][data-link-attribute="custom-value"]': 1 + 'a#disabled-link > svg:first-child': 1 + 'a[href="/custom-icon"].d-inline-flex.gap-2-5 > svg:last-child.icon--xl.icon--flip-vertical.custom-link-icon': 1 + 'a[href="/no-spacing"] > svg': 1 + 'a[href="/no-spacing"].gap-2-5': 0 + 'a[href="/no-spacing"] > svg.ms-2-5': 0 + 'a[href="/no-spacing"] > svg.me-2-5': 0 + equals: + 'h3 a em': information + 'a#disabled-link strong': link + contains: + 'h3 a svg': '/custom/icons.svg#arrow-right' + 'a#disabled-link svg': '/custom/icons.svg#lock' + 'a[href="/custom-icon"] svg': '/other/icons.svg#custom-symbol' + 'a[href="/no-spacing"] svg': '/custom/icons.svg#arrow-right' + +links_block_without_title: + render: + '#type': component + '#component': oe_bootstrap_theme:links_block + '#props': + links: + - label: 'Single link' + path: '/single-link' + assertions: + count: + 'div.bcl-links-block.bg-light': 1 + 'div.bcl-links-block h2.bcl-heading': 0 + 'ul > li.list-unstyled': 1 + 'a[href="/single-link"]': 1 + equals: + 'a[href="/single-link"]': 'Single link' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/list_group.yml b/tests/src/Kernel/fixtures/markup_rendering_components/list_group.yml new file mode 100644 index 000000000..bed6b4fda --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/list_group.yml @@ -0,0 +1,225 @@ +list_group_default_props_omitted: + render: + '#type': component + '#component': oe_bootstrap_theme:list_group + '#props': {} + assertions: + count: + 'ul[data-component-id="oe_bootstrap_theme:list_group"].list-group': 1 + 'ul.list-group-flush': 0 + 'ul.list-group-numbered': 0 + 'ul[class*="list-group-horizontal"]': 0 + 'ul > li': 0 + +list_group_unordered_with_variants_and_badges: + render: + '#type': component + '#component': oe_bootstrap_theme:list_group + '#props': + items: + - html: + '#markup': 'Item with rendered markup' + variant: danger + extra_classes: custom-item-class + - html: 'Item with a badge' + variant: secondary + badge: + label: '10' + background: success + rounded_pill: true + - html: 'Item with a linked badge' + badge: + label: + '#markup': 'New items' + background: info + url: '/updates' + title: 'View updates' + dismissible: true + assistive_text: 'Unread: ' + outline: true + attributes: + class: + - custom-badge-class + data-badge-attribute: custom-value + assertions: + count: + 'ul.list-group > li.list-group-item': 3 + 'li.list-group-item-danger.custom-item-class strong': 1 + 'li.list-group-item-secondary span.badge.text-bg-success.rounded-pill': 1 + 'li:nth-child(3) a.badge.badge-outline-info.custom-badge-class[href="/updates"][title="View updates"][data-badge-attribute="custom-value"]': 1 + 'li:nth-child(3) a.badge span.visually-hidden': 1 + 'li:nth-child(3) a.badge svg.icon--xs': 1 + equals: + 'li.list-group-item-danger strong': 'rendered markup' + 'li.list-group-item-secondary span.badge': '10' + 'li:nth-child(3) a.badge span.visually-hidden': 'Unread:' + contains: + 'li:nth-child(3) a.badge': 'New items' + 'li:nth-child(3) a.badge svg': 'assets/icons/bcl-default-icons.svg#x-circle-fill' + +list_group_ordered_horizontal_flush_with_custom_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:list_group + '#props': + type: ordered + flush: true + horizontal: horizontal-md + id: ordered-list-group + attributes: + class: + - custom-list-group + data-list-group-attribute: custom-value + items: + - html: 'First step' + - html: 'Second step' + - html: 'Third step' + assertions: + count: + 'ol#ordered-list-group.list-group.list-group-numbered.list-group-flush.list-group-horizontal-md.custom-list-group[data-list-group-attribute="custom-value"]': 1 + 'ol#ordered-list-group > li.list-group-item': 3 + 'ul.list-group': 0 + equals: + 'ol > li:nth-child(1)': 'First step' + 'ol > li:nth-child(3)': 'Third step' + +list_group_actionable_links_and_buttons: + render: + '#type': component + '#component': oe_bootstrap_theme:list_group + '#props': + type: actionable + items: + - label: + '#markup': 'Linked item' + path: '/linked-item' + variant: warning + icon: arrow-right + icon_position: before + icon_display: inline-flex + attributes: + class: + - custom-action-link + data-action-attribute: link + - label: 'Disabled standalone link' + path: '/disabled-item' + disabled: true + standalone: true + - button: true + label: 'Submit item' + type: submit + disabled: true + text_nowrap: true + assistive_text: 'Action: ' + icon: + name: check + transformation: rotate-90 + attributes: + class: + - custom-button-icon + icon_position: after + icon_spacing: lg + show_spinner: true + spinner: + type: grow + variant: success + size: sm + assistive_text: Loading + attributes: + class: + - custom-spinner + attributes: + class: + - custom-action-button + data-action-attribute: button + assertions: + count: + 'div.list-group[data-component-id="oe_bootstrap_theme:list_group"]': 1 + 'div.list-group > a.list-group-item.list-group-item-action.link-warning.custom-action-link[href="/linked-item"][data-action-attribute="link"]': 1 + 'a.custom-action-link.d-inline-flex.gap-2-5 > svg:first-child': 1 + 'a.custom-action-link strong': 1 + 'a[href="/disabled-item"].disabled.standalone[aria-disabled="true"][tabindex="-1"]': 1 + 'button.list-group-item.list-group-item-action.custom-action-button[type="submit"][disabled][aria-disabled="true"][data-action-attribute="button"]': 1 + 'button.text-nowrap.gap-lg-2-5': 1 + 'button > svg.icon--fluid.icon--rotate-90.custom-button-icon': 1 + 'button > div.spinner-grow.spinner-grow-sm.text-success.custom-spinner[role="status"]': 1 + 'button > div.spinner-grow span.visually-hidden': 1 + 'button > span.visually-hidden': 1 + 'button.btn': 0 + equals: + 'a.custom-action-link strong': item + 'button > div.spinner-grow span.visually-hidden': Loading + 'button > span.visually-hidden': 'Action:' + contains: + 'a.custom-action-link svg': 'assets/icons/bcl-default-icons.svg#arrow-right' + 'button > svg': 'assets/icons/bcl-default-icons.svg#check' + +list_group_with_form_inputs: + render: + '#type': component + '#component': oe_bootstrap_theme:list_group + '#props': + items: + - input: true + input_type: checkbox + id: list-group-checkbox + label: 'Checked switch' + required: true + checked: true + switch: true + attributes: + class: + - custom-checkbox + data-input-attribute: checkbox + - input: true + input_type: text + id: list-group-text + label: 'Horizontal text input' + hidden_label: true + disabled: true + readonly: true + valid: true + valid_feedback: 'The value is valid.' + helper_text: 'Supporting text.' + helper_text_id: list-group-text-help + placeholder: 'Enter a value' + horizontal: true + horizontal_class: col-sm-8 + horizontal_label_class: col-sm-4 + - input: true + input_type: text + id: list-group-invalid + label: 'Invalid floating input' + placeholder: 'Enter a value' + floating: true + invalid: true + invalid_feedback: 'Enter a valid value.' + - input: true + input_type: checkbox + id: list-group-toggle + label: 'Toggle input' + checked: true + toggle: true + toggle_variant: danger + remove_wrapper: true + assertions: + count: + 'ul.list-group > li.list-group-item': 4 + 'input#list-group-checkbox.form-check-input.custom-checkbox[type="checkbox"][required][checked][data-input-attribute="checkbox"]': 1 + 'input#list-group-checkbox + label.form-check-label': 1 + 'input#list-group-text.form-control.is-valid[type="text"][disabled][readonly][placeholder="Enter a value"][aria-describedby="list-group-text-help"]': 1 + 'label[for="list-group-text"].col-form-label.col-sm-4.visually-hidden': 1 + 'li:nth-child(2) div.col-sm-8': 1 + 'li:nth-child(2) div.valid-feedback': 1 + 'li:nth-child(2) div.form-text#list-group-text-help': 1 + 'li:nth-child(3) div.form-floating': 1 + 'input#list-group-invalid.form-control.is-invalid': 1 + 'li:nth-child(3) div.invalid-feedback': 1 + 'input#list-group-toggle.btn-check[type="checkbox"][checked]': 1 + 'label[for="list-group-toggle"].btn.btn-danger': 1 + equals: + 'label[for="list-group-checkbox"]': 'Checked switch' + 'li:nth-child(2) div.valid-feedback': 'The value is valid.' + 'li:nth-child(2) div.form-text': 'Supporting text.' + 'li:nth-child(3) div.invalid-feedback': 'Enter a valid value.' + 'label[for="list-group-toggle"]': 'Toggle input' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/listing.yml b/tests/src/Kernel/fixtures/markup_rendering_components/listing.yml new file mode 100644 index 000000000..a5e6fe68f --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/listing.yml @@ -0,0 +1,279 @@ +listing_default_complete: + render: + '#type': component + '#component': oe_bootstrap_theme:listing + '#props': + variant: default + columns: 1 + title: 'Latest publications' + title_tag: h3 + title_attributes: + class: + - custom-listing-title + data-title-attribute: custom-value + title_link: + path: '/publications' + variant: info + id: listing-title-link + standalone: true + icon: + name: arrow-up-right + size: xs + icon_position: after + icon_display: inline-flex + attributes: + data-title-link: custom-value + alignment: center + image_size: large + icon_path: '/custom/icons.svg' + attributes: + id: custom-listing + class: + - custom-listing-class + data-listing: custom-value + items: + - title: 'First publication' + title_tag: h4 + title_link: + path: '/publication/first' + variant: danger + id: first-publication-link + icon: + name: file-text + size: sm + icon_position: before + remove_icon_spacers: true + attributes: + class: + - custom-card-link + subtitle: 'Research report' + text: + '#markup': 'A publication with rendered text.' + image: + src: 'https://example.com/publication.jpg' + alt: 'Publication cover' + classes: custom-image-class + rounded: '2' + image_copyright: '© European Union, 2026' + image_copyright_label: 'Cover credit:' + content: + '#markup': 'Published 24 July 2026' + header: + '#markup': 'Featured' + footer: + '#markup': 'Publication footer' + badges: + - 'Report' + - label: 'Open data' + background: success + url: '/open-data' + title: 'Open data badge' + dismissible: true + assistive_text: 'Data type:' + rounded_pill: true + outline: true + attributes: + class: + - custom-badge-class + - title: + '#markup': 'Consultation deadline' + text: 'The second item uses a date block instead of an image.' + date: + day: 12 + month: Sep + year: 2026 + end_day: 18 + end_month: Sep + end_year: 2026 + date_time: '2026-09-12' + attributes: + id: consultation-date + class: + - custom-date-class + link: + label: 'View all publications' + path: '/publications/all' + variant: success + id: listing-cta + disabled: true + standalone: true + icon_position: before + icon_display: inline-flex + remove_icon_spacers: true + icon: + transformation: rotate-90 + attributes: + class: + - custom-cta-icon + attributes: + class: + - custom-cta-class + data-cta: custom-value + assertions: + count: + 'div#custom-listing[data-component-id="oe_bootstrap_theme:listing"].bcl-listing.bcl-listing--default-1-col.custom-listing-class[data-listing="custom-value"]': 1 + 'div#custom-listing.text-break': 0 + 'h3.bcl-heading': 1 + 'h3 > a#listing-title-link.link-info.standalone.d-inline-flex.gap-2-5[href="/publications"][data-title-link="custom-value"]': 1 + 'h3 > a#listing-title-link svg.icon--xs': 1 + 'h3 > a#listing-title-link use[xlink\:href="/custom/icons.svg#arrow-up-right"]': 1 + 'div.row.row-cols-1.g-4': 1 + 'article.listing-item': 2 + '.bcl-listing > .row > .col:first-child > article.listing-item .bcl-card-start-col.bcl-size-large.align-self-center': 1 + '.bcl-listing > .row > .col:first-child > article.listing-item .col-12.col-md.align-self-center': 1 + '.bcl-listing > .row > .col:first-child > article.listing-item .card-header .custom-card-header': 1 + '.bcl-listing > .row > .col:first-child > article.listing-item .card-footer .custom-card-footer': 1 + '.bcl-listing > .row > .col:first-child > article.listing-item img[src="https://example.com/publication.jpg"][alt="Publication cover"].custom-image-class.rounded-2': 1 + '.bcl-listing > .row > .col:first-child > article.listing-item .bcl-copyright .visually-hidden': 1 + '.bcl-listing > .row > .col:first-child > article.listing-item h4.card-title a#first-publication-link.custom-card-link.link-danger[href="/publication/first"]': 1 + '.bcl-listing > .row > .col:first-child > article.listing-item h4.card-title a#first-publication-link svg.icon--sm': 1 + '.bcl-listing > .row > .col:first-child > article.listing-item .card-text strong': 1 + '.bcl-listing > .row > .col:first-child > article.listing-item .custom-card-content': 1 + '.bcl-listing > .row > .col:first-child > article.listing-item span.badge.text-bg-primary': 1 + '.bcl-listing > .row > .col:first-child > article.listing-item a.badge.badge-outline-success.rounded-pill.custom-badge-class[href="/open-data"][title="Open data badge"]': 1 + '.bcl-listing > .row > .col:first-child > article.listing-item a.badge .visually-hidden': 1 + '.bcl-listing > .row > .col:first-child > article.listing-item a.badge svg.icon--xs': 1 + 'time#consultation-date.custom-date-class[datetime="2026-09-12"]': 1 + 'time#consultation-date use[xlink\:href="/custom/icons.svg#chevron-right"]': 1 + 'a#listing-cta.link-success.disabled.custom-cta-class[data-cta="custom-value"][href="/publications/all"][aria-disabled="true"][tabindex="-1"]': 1 + 'a#listing-cta svg.icon--rotate-90.custom-cta-icon': 1 + 'a#listing-cta use[xlink\:href="/custom/icons.svg#chevron-right"]': 1 + contains: + '.bcl-listing > .row > .col:first-child > article.listing-item .bcl-copyright': '© European Union, 2026' + 'a#listing-cta': 'View all publications' + 'h3 > a': 'Latest publications' + '.bcl-listing > .row > .col:first-child > article.listing-item a.badge': 'Open data' + '.bcl-listing > .row > .col:first-child > article.listing-item a.badge .visually-hidden': 'Data type:' + equals: + '.bcl-listing > .row > .col:first-child > article.listing-item .visually-hidden': 'Cover credit:' + '.bcl-listing > .row > .col:first-child > article.listing-item span.badge.text-bg-primary': 'Report' + +listing_default_two_columns: + render: + '#type': component + '#component': oe_bootstrap_theme:listing + '#props': + variant: default + columns: 2 + items: + - title: 'First card' + text: 'First card content.' + - title: 'Second card' + text: 'Second card content.' + assertions: + count: + '.bcl-listing.bcl-listing--default-2-col': 1 + '.row.row-cols-1.row-cols-md-2.g-4': 1 + 'article.listing-item': 2 + +listing_default_three_columns: + render: + '#type': component + '#component': oe_bootstrap_theme:listing + '#props': + variant: default + columns: 3 + items: + - title: 'First card' + - title: 'Second card' + - title: 'Third card' + assertions: + count: + '.bcl-listing.bcl-listing--default-3-col': 1 + '.row.row-cols-1.row-cols-md-2.row-cols-xl-3.g-4': 1 + 'article.listing-item': 3 + +listing_highlight_one_column: + render: + '#type': component + '#component': oe_bootstrap_theme:listing + '#props': + variant: highlight + columns: 1 + alignment: end + items: + - title: 'Highlighted card' + image: + src: 'https://example.com/highlight.jpg' + alt: 'Highlighted card image' + image_copyright: '© European Union, 2026' + text: 'Highlighted card content.' + assertions: + count: + '.bcl-listing.bcl-listing--highlight-1-col': 1 + '.row.row-cols-1.g-4': 1 + 'article.listing-item--highlight.bg-lighter.border-0': 1 + 'article.listing-item--highlight img.card-img-top': 1 + equals: + 'article.listing-item--highlight .bcl-copyright .visually-hidden': 'Image credit:' + +listing_highlight_two_columns: + render: + '#type': component + '#component': oe_bootstrap_theme:listing + '#props': + variant: highlight + columns: 2 + items: + - title: 'First highlighted card' + - title: 'Second highlighted card' + assertions: + count: + '.bcl-listing.bcl-listing--highlight-2-col': 1 + '.row.row-cols-1.row-cols-md-2.g-4': 1 + 'article.listing-item--highlight.bg-lighter.h-100.rounded-2': 2 + +listing_highlight_three_columns: + render: + '#type': component + '#component': oe_bootstrap_theme:listing + '#props': + variant: highlight + columns: 3 + items: + - title: 'First highlighted card' + - title: 'Second highlighted card' + - title: 'Third highlighted card' + assertions: + count: + '.bcl-listing.bcl-listing--highlight-3-col': 1 + '.row.row-cols-1.row-cols-md-3.g-4': 1 + 'article.listing-item--highlight.bg-lighter.h-100.rounded-2': 3 + +listing_optional_props_omitted: + render: + '#type': component + '#component': oe_bootstrap_theme:listing + '#props': + variant: default + columns: 1 + assertions: + count: + '.bcl-listing.bcl-listing--default-1-col.text-break': 1 + '.bcl-listing .row.row-cols-1.g-4': 1 + '.bcl-listing .bcl-heading': 0 + '.bcl-listing article.card': 0 + '.bcl-listing > a': 0 + +listing_title_label_and_custom_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:listing + '#props': + variant: default + columns: 1 + title: 'Archive heading' + title_link: + label: 'Browse the archive' + path: '/archive-overview' + attributes: + class: + - custom-listing-attributes + assertions: + count: + '.bcl-listing.bcl-listing--default-1-col.custom-listing-attributes': 1 + '.bcl-listing.text-break': 0 + '.bcl-listing h2 > a[href="/archive-overview"]': 1 + '.bcl-listing > a': 0 + equals: + '.bcl-listing h2 > a[href="/archive-overview"]': 'Browse the archive' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/modal.yml b/tests/src/Kernel/fixtures/markup_rendering_components/modal.yml new file mode 100644 index 000000000..0d1f45646 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/modal.yml @@ -0,0 +1,157 @@ +modal_default_with_fallback_id: + render: + '#type': component + '#component': oe_bootstrap_theme:modal + '#props': + body: 'Default modal body.' + toggle: + label: 'Open modal' + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:modal"]#modal.modal.fade[tabindex="-1"][aria-hidden="true"]': 1 + 'div#modal > div.modal-dialog': 1 + 'div#modal > div.modal-dialog.modal-sm': 0 + 'div#modal > div.modal-dialog.modal-lg': 0 + 'div#modal > div.modal-dialog.modal-xl': 0 + 'div#modal > div.modal-dialog.modal-fullscreen': 0 + 'div#modal > div.modal-dialog.modal-dialog-centered': 0 + 'div#modal > div.modal-dialog.modal-dialog-scrollable': 0 + 'div#modal[data-bs-backdrop]': 0 + 'div#modal .modal-header': 0 + 'div#modal .modal-footer': 0 + 'button.btn-primary[data-bs-toggle="modal"][data-bs-target="#modal"][aria-controls="modal"]': 1 + equals: + 'div#modal .modal-body': 'Default modal body.' + 'button[data-bs-toggle="modal"]': 'Open modal' + +modal_with_all_content_and_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:modal + '#props': + modal_id: custom-modal + size: sm + static_backdrop: true + vertically_centered: true + scrollable: true + title: + - '#markup': 'Custom modal' + title_tag: h4 + title_attributes: + class: + - custom-modal-title + data-modal-title: custom-value + show_close_button: true + messages: + - '#markup': '
Status message
' + body: + - '#type': html_tag + '#tag': p + '#value': 'Custom modal body.' + footer: + - '#type': html_tag + '#tag': button + '#attributes': + class: + - btn + - btn-secondary + data-bs-dismiss: modal + '#value': Cancel + attributes: + class: + - custom-modal + data-modal: custom-value + header_attributes: + id: custom-modal-header + class: + - custom-modal-header + body_attributes: + id: custom-modal-body + class: + - custom-modal-body + footer_attributes: + id: custom-modal-footer + class: + - custom-modal-footer + toggle: + id: custom-modal-toggle + label: 'Open custom modal' + variant: danger + size: lg + outline: true + assistive_text: 'Dialog:' + icon: + name: box-arrow-up-right + path: /custom-icons.svg + attributes: + class: + - custom-toggle-icon + icon_position: before + attributes: + class: + - custom-toggle + data-toggle: custom-value + assertions: + count: + 'div#custom-modal.modal.fade.custom-modal[data-modal="custom-value"][data-bs-backdrop="static"]': 1 + 'div#custom-modal > div.modal-dialog.modal-sm.modal-dialog-centered.modal-dialog-scrollable': 1 + 'div#custom-modal .modal-header#custom-modal-header.custom-modal-header': 1 + 'div#custom-modal .modal-header h4.custom-modal-title[data-modal-title="custom-value"] em': 1 + 'div#custom-modal .modal-header h5': 0 + 'div#custom-modal .modal-header button.btn-close[data-bs-dismiss="modal"][aria-label="Close"]': 1 + 'div#custom-modal .alert.alert-info': 1 + 'div#custom-modal .modal-body#custom-modal-body.custom-modal-body > p': 1 + 'div#custom-modal .modal-footer#custom-modal-footer.custom-modal-footer > button[data-bs-dismiss="modal"]': 1 + 'button#custom-modal-toggle.btn-lg.btn-outline-danger.custom-toggle[data-toggle="custom-value"]': 1 + 'button#custom-modal-toggle[data-bs-toggle="modal"][data-bs-target="#custom-modal"][aria-controls="custom-modal"]': 1 + 'button#custom-modal-toggle svg.custom-toggle-icon': 1 + 'button#custom-modal-toggle > span.visually-hidden': 1 + contains: + 'button#custom-modal-toggle svg': '/custom-icons.svg#box-arrow-up-right' + equals: + 'div#custom-modal .modal-header h4': 'Custom modal' + 'div#custom-modal .alert': 'Status message' + 'div#custom-modal .modal-body p': 'Custom modal body.' + 'div#custom-modal .modal-footer button': Cancel + 'button#custom-modal-toggle > span.visually-hidden': 'Dialog:' + +modal_attribute_id_takes_precedence: + render: + '#type': component + '#component': oe_bootstrap_theme:modal + '#props': + modal_id: ignored-modal-id + size: fullscreen + title: 'Fullscreen modal' + body: 'Fullscreen modal body.' + attributes: + id: attribute-modal-id + toggle: + label: 'Open fullscreen modal' + disabled: true + assertions: + count: + 'div#attribute-modal-id.modal': 1 + 'div#ignored-modal-id': 0 + 'div#attribute-modal-id > div.modal-dialog.modal-fullscreen': 1 + 'div#attribute-modal-id .modal-header h5': 1 + 'div#attribute-modal-id .modal-header .btn-close': 0 + 'button[data-bs-target="#attribute-modal-id"][aria-controls="attribute-modal-id"][disabled][aria-disabled="true"]': 1 + +modal_responsive_fullscreen_without_optional_regions: + render: + '#type': component + '#component': oe_bootstrap_theme:modal + '#props': + modal_id: responsive-modal + fullscreen_responsive: md + body: 'Responsive modal body.' + assertions: + count: + 'div#responsive-modal > div.modal-dialog.modal-fullscreen-md-down': 1 + 'div#responsive-modal .modal-header': 0 + 'div#responsive-modal .modal-body': 1 + 'div#responsive-modal .modal-footer': 0 + 'button[data-bs-toggle="modal"]': 0 + equals: + 'div#responsive-modal .modal-body': 'Responsive modal body.' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/navbar.yml b/tests/src/Kernel/fixtures/markup_rendering_components/navbar.yml new file mode 100644 index 000000000..e1708c537 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/navbar.yml @@ -0,0 +1,133 @@ +navbar_default_props_omitted: + render: + '#type': component + '#component': oe_bootstrap_theme:navbar + '#props': {} + assertions: + count: + 'nav[data-component-id="oe_bootstrap_theme:navbar"].navbar.navbar-expand-lg.navbar-light.bg-light': 1 + 'nav > div.container': 1 + 'button.navbar-toggler': 0 + 'div.navbar-collapse': 0 + equals: + 'nav > div.container': '' + +navbar_light_with_all_regions: + render: + '#type': component + '#component': oe_bootstrap_theme:navbar + '#props': + expanded_breakpoint: lg + branding: + '#type': html_tag + '#tag': a + '#value': 'OpenEuropa Site' + '#attributes': + href: 'https://europa.eu/' + rel: home + class: + - navbar-brand + navigation: + - '#markup': 'Priorities' + - '#type': html_tag + '#tag': a + '#value': Funding + '#attributes': + href: '#funding' + class: + - nav-link + right: + '#type': html_tag + '#tag': form + '#attributes': + class: + - d-flex + action: /search + '#children': '' + assertions: + count: + 'nav.navbar.navbar-expand-lg.navbar-light.bg-light': 1 + 'nav.navbar-expand-sm': 0 + 'nav.navbar-expand-md': 0 + 'nav > .container > a.navbar-brand[href="https://europa.eu/"][rel="home"]': 1 + 'button.navbar-toggler[type="button"][data-bs-toggle="collapse"][aria-expanded="false"][aria-label="Toggle navigation"]': 1 + 'button.navbar-toggler[data-bs-target^="#bcl-navbar"][aria-controls^="bcl-navbar"]': 1 + 'div.navbar-collapse[id^="bcl-navbar"]': 1 + 'div.navbar-collapse > a.nav-link': 2 + 'div.navbar-collapse > a.nav-link[href="#priorities"]': 1 + 'div.navbar-collapse > a.nav-link[href="#funding"]': 1 + 'div.navbar-collapse > form.d-flex[action="/search"]': 1 + 'form.d-flex input#navbar-search[name="search"]': 1 + equals: + 'a.navbar-brand': 'OpenEuropa Site' + 'a[href="#priorities"]': Priorities + 'a[href="#funding"]': Funding + 'form.d-flex label': Search + +navbar_dark_with_custom_id_and_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:navbar + '#props': + collapse_id: custom-navbar-collapse + expanded_breakpoint: md + color_scheme: dark + branding: + '#markup': 'Digital Europe' + navigation: + '#type': html_tag + '#tag': a + '#value': 'About the programme' + '#attributes': + href: /about + class: + - nav-link + right: + '#type': html_tag + '#tag': button + '#value': Account + '#attributes': + class: + - btn + - btn-light + type: button + attributes: + id: custom-navbar + class: + - custom-navbar + aria-label: 'Primary navigation' + data-navbar-attribute: custom-value + assertions: + count: + 'nav#custom-navbar.navbar.navbar-expand-md.navbar-dark.bg-dark.custom-navbar[aria-label="Primary navigation"][data-navbar-attribute="custom-value"]': 1 + 'nav.navbar-expand-lg': 0 + 'button.navbar-toggler[data-bs-target="#custom-navbar-collapse"][aria-controls="custom-navbar-collapse"]': 1 + 'div#custom-navbar-collapse.collapse.navbar-collapse': 1 + 'a.navbar-brand[href="/home"] > strong': 1 + 'div.navbar-collapse > a.nav-link[href="/about"]': 1 + 'div.navbar-collapse > button.btn.btn-light[type="button"]': 1 + equals: + 'a.navbar-brand strong': 'Digital Europe' + 'a.nav-link': 'About the programme' + 'button.btn-light': Account + +navbar_without_background_or_collapse: + render: + '#type': component + '#component': oe_bootstrap_theme:navbar + '#props': + expanded_breakpoint: sm + background: false + disable_collapse: true + navigation: + '#markup': 'Direct link' + assertions: + count: + 'nav.navbar.navbar-expand-sm.navbar-light': 1 + 'nav.bg-light': 0 + 'nav.bg-dark': 0 + 'button.navbar-toggler': 0 + 'div.navbar-collapse': 0 + 'nav > .container > a.nav-link[href="/direct-link"]': 1 + equals: + 'a.nav-link': 'Direct link' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/navigation.yml b/tests/src/Kernel/fixtures/markup_rendering_components/navigation.yml new file mode 100644 index 000000000..a2739e428 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/navigation.yml @@ -0,0 +1,231 @@ +navigation_default_with_mixed_items: + render: + '#type': component + '#component': oe_bootstrap_theme:navigation + '#props': + variant: default + icon_path: /custom-icons.svg + items: + - label: + - '#type': html_tag + '#tag': strong + '#value': Overview + path: /overview + attributes: + class: + - custom-overview-link + data-navigation-item: overview + - label: 'Action button' + button: true + active: true + show_spinner: true + spinner: + type: border + size: sm + assistive_text: Loading + attributes: + class: + - custom-navigation-spinner + attributes: + class: + - custom-action-button + - label: 'Disabled link' + path: /disabled + disabled: true + - label: Calendar + path: /calendar + icon: + name: calendar-event + transformation: rotate-90 + attributes: + class: + - custom-navigation-icon + icon_position: before + assertions: + count: + 'ul[data-component-id="oe_bootstrap_theme:navigation"].nav': 1 + 'nav[data-component-id="oe_bootstrap_theme:navigation"]': 0 + 'ul.nav-pills': 0 + 'ul.nav-tabs': 0 + 'ul.navbar-nav': 0 + 'ul.flex-column': 0 + 'ul.nav-fill': 0 + 'ul > li.nav-item': 4 + 'li:nth-child(1) > a.nav-link.custom-overview-link[data-navigation-item="overview"] strong': 1 + 'li:nth-child(2) > button.nav-link.custom-action-button.active': 1 + 'li:nth-child(2) > button > div.spinner-border.spinner-border-sm.custom-navigation-spinner[role="status"]': 1 + 'li:nth-child(2) > button > div.spinner-border span.visually-hidden': 1 + 'li:nth-child(2) > button.btn': 0 + 'li:nth-child(3) > a.nav-link.disabled[href="/disabled"][aria-disabled="true"][tabindex="-1"]': 1 + 'li:nth-child(4) > a.nav-link[href="/calendar"] svg.icon--rotate-90.custom-navigation-icon': 1 + contains: + 'li:nth-child(4) svg': '/custom-icons.svg#calendar-event' + 'li:nth-child(2) button': 'Action button' + equals: + 'li:nth-child(1) strong': Overview + +navigation_vertical_pills_dropdown_and_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:navigation + '#props': + variant: pills + orientation: vertical + navigation_id: custom-navigation + full_width: true + alignment: end + attributes: + class: + - custom-navigation + data-navigation: custom-value + items: + - label: Profile + path: '#profile' + active: true + - label: Settings + button: true + - dropdown: true + link: true + id: navigation-dropdown + alignment: dropdown-menu-end + dark_mode: true + attributes: + class: + - custom-dropdown-menu + trigger: + label: More + path: '#more' + attributes: + class: + - custom-dropdown-trigger + items: + - label: 'Dropdown link' + path: /dropdown-link + attributes: + class: + - custom-dropdown-link + - label: 'Dropdown button' + button: true + active: true + - divider: true + - label: 'Disabled dropdown button' + button: true + disabled: true + assertions: + count: + 'nav#custom-navigation.nav.nav-pills.flex-column.nav-fill.justify-content-end.custom-navigation[data-navigation="custom-value"]': 1 + 'nav#custom-navigation > a.nav-link.active[href="#profile"]': 1 + 'nav#custom-navigation > button.nav-link': 1 + 'nav#custom-navigation > a#navigation-dropdown.dropdown-toggle.custom-dropdown-trigger[href="#more"][data-bs-toggle="dropdown"]': 1 + 'nav#custom-navigation > ul.dropdown-menu.dropdown-menu-end.dropdown-menu-dark.custom-dropdown-menu': 1 + 'ul.dropdown-menu > li': 4 + 'a.dropdown-item.custom-dropdown-link[href="/dropdown-link"]': 1 + 'button.dropdown-item.active': 1 + 'hr.dropdown-divider': 1 + 'button.dropdown-item[disabled][aria-disabled="true"]': 1 + equals: + 'nav#custom-navigation > button.nav-link': Settings + 'a.custom-dropdown-link': 'Dropdown link' + +navigation_tabs_with_content: + render: + '#type': component + '#component': oe_bootstrap_theme:navigation + '#props': + variant: tabs + navigation_id: tab-navigation + tabs_content: true + items: + - id: overview-tab + label: Overview + path: '#overview-panel' + target: overview-panel + active: true + content: + - '#markup': '

Overview content

' + attributes: + data-tab: overview + - id: details-tab + label: Details + path: '#details-panel' + target: details-panel + content: + - '#type': html_tag + '#tag': strong + '#value': 'Details content' + assertions: + count: + 'nav#tab-navigation.nav.nav-tabs[role="tablist"]': 1 + 'nav#tab-navigation > a.nav-link[role="tab"]': 2 + 'nav#tab-navigation > a#overview-tab.nav-link.active[href="#overview-panel"][data-tab="overview"]': 1 + 'div.tab-content': 1 + 'div.tab-content > div#overview-panel.tab-pane.fade.show.active[role="tabpanel"][aria-labelledby="overview-tab"] p.overview-content': 1 + 'div.tab-content > div#details-panel.tab-pane.fade[role="tabpanel"][aria-labelledby="details-tab"] strong': 1 + equals: + 'div#overview-panel p': 'Overview content' + 'div#details-panel strong': 'Details content' + +navigation_navbar_with_attribute_id: + render: + '#type': component + '#component': oe_bootstrap_theme:navigation + '#props': + variant: navbar + alignment: center + attributes: + id: navbar-navigation + class: + - custom-navbar-navigation + items: + - label: Home + path: /home + - label: Contact + path: /contact + assertions: + count: + 'ul#navbar-navigation.navbar-nav.justify-content-center.custom-navbar-navigation': 1 + 'ul#navbar-navigation.nav': 0 + 'ul#navbar-navigation > li.nav-item': 2 + 'li:nth-child(1) > a.nav-link[href="/home"]': 1 + 'li:nth-child(2) > a.nav-link[href="/contact"]': 1 + +navigation_nested: + render: + '#type': component + '#component': oe_bootstrap_theme:navigation + '#props': + variant: default + icon_path: /nested-icons.svg + items: + - label: 'Parent section' + path: '#parent' + navigation: + id: child-navigation + variant: pills + orientation: vertical + attributes: + class: + - custom-child-navigation + data-level: child + items: + - label: 'Child link' + path: '#child-link' + attributes: + class: + - custom-child-link + - label: 'Child action' + button: true + icon: arrow-right + icon_position: after + assertions: + count: + 'ul[data-component-id="oe_bootstrap_theme:navigation"].nav > li.nav-item': 1 + 'li.nav-item > a.nav-link[href="#parent"]': 1 + 'li.nav-item > nav#child-navigation.nav.nav-pills.flex-column.custom-child-navigation[data-level="child"]': 1 + 'nav#child-navigation > a.nav-link.custom-child-link[href="#child-link"]': 1 + 'nav#child-navigation > button.nav-link': 1 + 'nav#child-navigation > button.nav-link svg': 1 + contains: + 'nav#child-navigation > button svg': '/nested-icons.svg#arrow-right' + equals: + 'nav#child-navigation > button': 'Child action' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/offcanvas.yml b/tests/src/Kernel/fixtures/markup_rendering_components/offcanvas.yml new file mode 100644 index 000000000..039ee1cac --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/offcanvas.yml @@ -0,0 +1,216 @@ +offcanvas_default: + render: + '#type': component + '#component': oe_bootstrap_theme:offcanvas + '#props': + variant: default + offcanvas_id: default-offcanvas + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:offcanvas"]#default-offcanvas.offcanvas-lg[tabindex="-1"][data-bs-backdrop="false"]': 1 + 'div.offcanvas-start': 0 + 'div.offcanvas-end': 0 + 'div.offcanvas-top': 0 + 'div.offcanvas-bottom': 0 + 'div.offcanvas-header': 1 + 'div.offcanvas-body': 0 + 'button.btn-close[data-bs-dismiss="offcanvas"][data-bs-target="#default-offcanvas"][aria-controls="default-offcanvas"][aria-label="Close"]': 1 + 'button[data-bs-toggle="offcanvas"]': 0 + +offcanvas_with_all_parameters: + render: + '#type': component + '#component': oe_bootstrap_theme:offcanvas + '#props': + variant: default + offcanvas_id: detailed-offcanvas + title: + '#markup': 'Linked offcanvas title' + title_tag: h3 + title_link: + path: '/offcanvas-title' + variant: success + id: custom-title-link-id + disabled: true + icon: + name: link + path: /title-icons.svg + transformation: flip-horizontal + attributes: + class: + - custom-title-icon + icon_position: before + icon_display: inline-flex + attributes: + class: + - custom-title-link + title: 'Title link tooltip' + target: _blank + data-title-link: custom-value + title_attributes: + class: + - custom-title + data-title: custom-value + placement: end + body: + '#type': html_tag + '#tag': strong + '#value': 'Rendered body content' + body_scroll: true + backdrop: true + close_aria_label: 'Close detailed panel' + extra_classes_body: custom-body + extra_classes_header: custom-header + extra_classes_close: custom-close + responsiveness: xl + toggle: + label: 'Open detailed panel' + variant: secondary + size: lg + type: button + outline: true + disabled: true + text_nowrap: true + assistive_text: 'Panel action' + id: custom-toggle + icon: + name: filter + path: /custom-icons.svg + transformation: rotate-90 + attributes: + class: + - custom-toggle-icon + data-icon: custom-value + icon_position: before + icon_spacing: lg + attributes: + class: + - custom-toggle + data-toggle: custom-value + attributes: + class: + - custom-offcanvas + aria-label: 'Detailed offcanvas' + data-offcanvas: custom-value + assertions: + count: + 'div#detailed-offcanvas.offcanvas-xl.offcanvas-end.custom-offcanvas[tabindex="-1"][aria-label="Detailed offcanvas"][data-offcanvas="custom-value"]': 1 + 'div#detailed-offcanvas[data-bs-scroll="true"]': 1 + 'div#detailed-offcanvas[data-bs-backdrop]': 0 + 'div#detailed-offcanvas[aria-labelledby="detailed-offcanvas-title"]': 1 + 'div.offcanvas-header.custom-header': 1 + 'h3#detailed-offcanvas-title.offcanvas-title.bcl-heading.custom-title[data-title="custom-value"]': 1 + 'h3 a#custom-title-link-id.custom-title-link.link-success.disabled[href="/offcanvas-title"][title="Title link tooltip"][target="_blank"][data-title-link="custom-value"][aria-disabled="true"][tabindex="-1"]': 1 + 'h3 a#custom-title-link-id.d-inline-flex.gap-2-5 > svg.icon--flip-horizontal.custom-title-icon': 1 + 'h3 a#custom-title-link-id > em': 1 + 'div.offcanvas-body.custom-body > strong': 1 + 'button.btn-close.custom-close[aria-label="Close detailed panel"]': 1 + 'button#custom-toggle.btn-outline-secondary.btn-lg.custom-toggle[data-bs-toggle="offcanvas"][data-bs-target="#detailed-offcanvas"][aria-controls="detailed-offcanvas"][data-toggle="custom-value"][disabled][aria-disabled="true"]': 1 + 'button#custom-toggle.text-nowrap': 1 + 'button#custom-toggle.gap-lg-2-5': 1 + 'button#custom-toggle > span.visually-hidden': 1 + 'button#custom-toggle > svg.icon--rotate-90.custom-toggle-icon[data-icon="custom-value"]': 1 + contains: + 'h3 a#custom-title-link-id svg': '/title-icons.svg#link' + 'button#custom-toggle svg': '/custom-icons.svg#filter' + equals: + 'h3 a em': 'Linked offcanvas title' + 'div.offcanvas-body strong': 'Rendered body content' + 'button#custom-toggle > span.visually-hidden': 'Panel action' + +offcanvas_without_close: + render: + '#type': component + '#component': oe_bootstrap_theme:offcanvas + '#props': + variant: default + offcanvas_id: offcanvas-without-close + placement: bottom + responsiveness: none + title: 'Persistent panel' + title_link: + path: '/persistent-panel' + icon: link + icon_position: before + remove_icon_spacers: true + body: 'Persistent panel content.' + with_close: false + assertions: + count: + 'div#offcanvas-without-close.offcanvas.offcanvas-bottom': 1 + 'div#offcanvas-without-close.offcanvas-md': 0 + 'div.offcanvas-header': 1 + 'button.btn-close': 0 + 'div.offcanvas-body': 1 + 'h2.offcanvas-title > a[href="/persistent-panel"] > svg': 1 + 'h2.offcanvas-title > a.gap-2-5': 0 + 'h2.offcanvas-title > a > svg.me-2-5': 0 + equals: + 'div.offcanvas-body': 'Persistent panel content.' + contains: + 'h2.offcanvas-title > a': 'Persistent panel' + 'h2.offcanvas-title > a svg': '#link' + +offcanvas_search_variant: + render: + '#type': component + '#component': oe_bootstrap_theme:offcanvas + '#props': + variant: search + offcanvas_id: search-offcanvas + placement: top + title: 'Search filters' + body: 'Filter controls' + extra_classes_body: custom-search-body + extra_classes_header: custom-search-header + extra_classes_close: custom-search-close + title_attributes: + class: + - custom-search-title + assertions: + count: + 'div#search-offcanvas.bcl-offcanvas.offcanvas-lg.offcanvas-top': 1 + 'div.offcanvas-header.p-lg-0.d-lg-block.custom-search-header': 1 + 'h2.offcanvas-title.mb-lg-4.custom-search-title': 1 + 'div.offcanvas-body.p-lg-0.d-block.custom-search-body': 1 + 'button.btn-close.d-lg-none.custom-search-close': 1 + equals: + 'h2.offcanvas-title': 'Search filters' + 'div.offcanvas-body': 'Filter controls' + +offcanvas_id_from_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:offcanvas + '#props': + variant: default + placement: start + attributes: + id: attribute-offcanvas-id + toggle: + label: 'Open attribute ID panel' + assertions: + count: + 'div#attribute-offcanvas-id.offcanvas-lg.offcanvas-start': 1 + 'button.btn-close[data-bs-target="#attribute-offcanvas-id"][aria-controls="attribute-offcanvas-id"]': 1 + 'button[data-bs-toggle="offcanvas"][data-bs-target="#attribute-offcanvas-id"][aria-controls="attribute-offcanvas-id"]': 1 + equals: + 'button[data-bs-toggle="offcanvas"]': 'Open attribute ID panel' + +offcanvas_with_generated_id_and_no_header_or_body: + render: + '#type': component + '#component': oe_bootstrap_theme:offcanvas + '#props': + variant: default + responsiveness: xxl + with_close: false + toggle: + label: 'Open generated ID panel' + assertions: + count: + 'div[id^="offcanvas"].offcanvas-xxl': 1 + 'div.offcanvas-header': 0 + 'div.offcanvas-body': 0 + 'button[data-bs-toggle="offcanvas"][data-bs-target^="#offcanvas"][aria-controls^="offcanvas"]': 1 + 'button.btn-close': 0 diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/pagination.yml b/tests/src/Kernel/fixtures/markup_rendering_components/pagination.yml new file mode 100644 index 000000000..1ab3f282a --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/pagination.yml @@ -0,0 +1,179 @@ +pagination_default: + render: + '#type': component + '#component': oe_bootstrap_theme:pagination + '#props': + variant: default + assertions: + count: + 'nav[data-component-id="oe_bootstrap_theme:pagination"][role="navigation"]': 1 + 'nav > ul.pagination.js-pager__items': 1 + 'ul.pagination--glossary': 0 + 'ul.pagination-sm': 0 + 'ul.pagination-lg': 0 + 'ul[class*="justify-content-"]': 0 + 'ul > li': 0 + +pagination_text_controls_and_custom_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:pagination + '#props': + variant: default + size: sm + alignment: center + enable_first_last_icons: false + first: + path: '/pages/first' + label: First + attributes: + aria-label: 'Go to the first page' + class: + - custom-first-link + prev: + path: '/pages/previous' + label: Previous + next: + path: '/pages/next' + label: Next + disabled: true + last: + path: '/pages/last' + label: Last + items: + - path: '/pages/1' + label: + '#markup': '1' + active: true + attributes: + aria-label: 'Page 1' + class: + - custom-page-link + data-page-attribute: custom-value + - path: '/pages/2' + label: '2' + disabled: true + list_attributes: + id: custom-pagination-list + class: + - custom-pagination-list + data-list-attribute: custom-value + attributes: + id: custom-pagination + class: + - custom-pagination + aria-label: 'Search results pages' + data-pagination-attribute: custom-value + assertions: + count: + 'nav#custom-pagination.custom-pagination[role="navigation"][aria-label="Search results pages"][data-pagination-attribute="custom-value"]': 1 + 'ul#custom-pagination-list.pagination.pagination-sm.justify-content-center.js-pager__items.custom-pagination-list[data-list-attribute="custom-value"]': 1 + 'ul > li.page-item': 6 + 'li.page-item.active > a.page-link.custom-page-link[href="/pages/1"][aria-current="page"][aria-label="Page 1"][data-page-attribute="custom-value"]': 1 + 'li.page-item.disabled > a.page-link.disabled[href="/pages/2"][aria-disabled="true"][tabindex="-1"]': 1 + 'li.page-item.disabled > a.page-link.disabled[href="/pages/next"]': 1 + 'a.custom-first-link[href="/pages/first"][aria-label="Go to the first page"]': 1 + 'a[href="/pages/previous"][aria-label="Previous"]': 1 + 'a[href="/pages/last"][aria-label="Last"]': 1 + 'a.page-link svg': 0 + equals: + 'a[href="/pages/first"]': First + 'a[href="/pages/previous"]': Previous + 'a[href="/pages/1"] strong': '1' + 'a[href="/pages/2"]': '2' + 'a[href="/pages/next"]': Next + 'a[href="/pages/last"]': Last + +pagination_icons_and_ellipsis: + render: + '#type': component + '#component': oe_bootstrap_theme:pagination + '#props': + variant: default + size: lg + alignment: end + enable_prev_next_icons: true + ellipsis_before: true + ellipsis_after: true + first: + path: '/pages/first' + label: First + prev: + path: '/pages/previous' + label: Previous + icon: + name: arrow-left + path: '/other/icons.svg' + size: xl + transformation: rotate-90 + attributes: + class: + - custom-previous-icon + data-icon-attribute: custom-value + next: + path: '/pages/next' + label: Next + last: + path: '/pages/last' + label: Last + items: + - path: '/pages/5' + label: '5' + active: true + icon_path: '/custom/icons.svg' + assertions: + count: + 'ul.pagination.pagination-lg.justify-content-end.js-pager__items': 1 + 'ul > li.page-item': 7 + 'li:nth-child(3) > div.page-link > span[aria-hidden="true"]': 1 + 'li:nth-child(5) > div.page-link > span[aria-hidden="true"]': 1 + 'a[href="/pages/first"] > svg.icon--fluid': 1 + 'a[href="/pages/previous"] > svg.icon--xl.icon--rotate-90.custom-previous-icon[data-icon-attribute="custom-value"]': 1 + 'a[href="/pages/next"] > svg.icon--fluid': 1 + 'a[href="/pages/last"] > svg.icon--fluid': 1 + 'li:nth-child(4).active > a[href="/pages/5"][aria-current="page"]': 1 + contains: + 'a[href="/pages/first"] svg': '/custom/icons.svg#chevron-double-left' + 'a[href="/pages/previous"] svg': '/other/icons.svg#arrow-left' + 'a[href="/pages/next"] svg': '/custom/icons.svg#chevron-right' + 'a[href="/pages/last"] svg': '/custom/icons.svg#chevron-double-right' + +pagination_glossary: + render: + '#type': component + '#component': oe_bootstrap_theme:pagination + '#props': + variant: glossary + items: + - path: '#a' + label: + '#markup': 'A' + attributes: + aria-label: 'Letter A' + class: + - custom-letter-link + - path: '#b' + label: B + active: true + - path: '#c' + label: C + - label: '0-9' + attributes: + class: + - custom-glossary + data-glossary-attribute: custom-value + assertions: + count: + 'nav.bcl-glossary.custom-glossary[role="navigation"][aria-label="Glossary links"][data-glossary-attribute="custom-value"]': 1 + 'ul.pagination.pagination--glossary.js-pager__items': 1 + 'ul > li.page-item': 4 + 'a.custom-letter-link[href="#a"][aria-label="Letter A"] > strong': 1 + 'li.active > a[href="#b"][aria-current="page"][aria-label="B"]': 1 + 'a[href="#c"][aria-label="C"]': 1 + 'li:nth-child(4) > div.page-link[aria-label="0-9"]': 1 + 'nav > ul > li > svg': 0 + equals: + 'a[href="#a"] strong': A + 'a[href="#b"]': B + 'a[href="#c"]': C + 'li:nth-child(4) > div.page-link': '0-9' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/pagination_v2.yml b/tests/src/Kernel/fixtures/markup_rendering_components/pagination_v2.yml new file mode 100644 index 000000000..582e3b352 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/pagination_v2.yml @@ -0,0 +1,178 @@ +pagination_v2_defaults: + render: + '#type': component + '#component': oe_bootstrap_theme:pagination_v2 + '#props': + prev: + path: '#previous' + label: Previous + next: + path: '#next' + label: Next + items: + - path: '#page-1' + label: '1' + - label: '2' + active: true + - path: '#page-3' + label: '3' + assertions: + count: + 'nav[data-component-id="oe_bootstrap_theme:pagination_v2"].pager.pagination-v2': 1 + 'nav[aria-label]': 0 + 'nav > ul.pagination.pager__items.js-pager__items.justify-content-md-center': 1 + 'nav > ul.pagination-sm': 0 + 'nav > ul.pagination-lg': 0 + 'nav > ul > li.page-item': 5 + 'nav > ul > li.pager__item--prev > a.page-link[href="#previous"][aria-label="Previous"]': 1 + 'nav > ul > li.pager__item--next > a.page-link[href="#next"][aria-label="Next"]': 1 + 'nav > ul > li.pager__item--prev svg': 1 + 'nav > ul > li.pager__item--next svg': 1 + 'nav > ul > li.active > div.page-link[aria-label="2"][aria-current="page"][tabindex="0"]': 1 + contains: + 'li.pager__item--prev svg': '#chevron-left' + 'li.pager__item--next svg': '#chevron-right' + equals: + 'li.active > div.page-link': '2' + +pagination_v2_text_controls_and_custom_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:pagination_v2 + '#props': + size: sm + alignment: end + aria_label: 'Search results pages' + enable_prev_next_icon: false + enable_first_last_icon: false + attributes: + id: custom-pagination + class: + - custom-pagination + data-pagination: custom-value + list_attributes: + id: custom-pagination-list + class: + - custom-pagination-list + data-pagination-list: custom-list-value + first: + path: '#first' + label: First + attributes: + id: first-page-link + class: + - custom-first-link + prev: + path: '#previous' + label: Previous + aria_label: 'Go to previous page' + next: + path: '#next' + label: Next + last: + path: '#last' + label: Last + items: + - path: '#page-1' + label: '1' + attributes: + class: + - custom-page-link + data-page: '1' + - path: '#page-2' + label: '2' + disabled: true + - label: + - '#type': html_tag + '#tag': strong + '#value': '3' + aria_label: 'Page 3' + active: true + assertions: + count: + 'nav#custom-pagination.pager.pagination-v2.custom-pagination[data-pagination="custom-value"][aria-label="Search results pages"]': 1 + 'nav > ul#custom-pagination-list.pagination.pagination-sm.justify-content-md-end.custom-pagination-list[data-pagination-list="custom-list-value"]': 1 + 'nav > ul > li.page-item': 7 + 'nav svg': 0 + 'a#first-page-link.page-link.custom-first-link[href="#first"][aria-label="First"]': 1 + 'a.page-link[href="#previous"][aria-label="Go to previous page"]': 1 + 'a.page-link.custom-page-link[href="#page-1"][data-page="1"]': 1 + 'li.disabled > a.page-link[href="#page-2"][aria-disabled="true"]': 1 + 'li.active > div.page-link[aria-label="Page 3"] strong': 1 + equals: + 'a#first-page-link': First + 'li.active > div.page-link strong': '3' + +pagination_v2_icons_ellipses_and_mobile_label: + render: + '#type': component + '#component': oe_bootstrap_theme:pagination_v2 + '#props': + size: lg + alignment: start + icon_path: /custom-icons.svg + enable_prev_next_icon: true + enable_first_last_icon: true + ellipsis_before: true + ellipsis_after: true + current_mobile_label: 'Page 6 of 10' + first: + path: '#first' + prev: + path: '#previous' + next: + path: '#next' + last: + path: '#last' + items: + - path: '#page-4' + label: '4' + - path: '#page-5' + label: '5' + - label: '6' + active: true + - path: '#page-7' + label: '7' + assertions: + count: + 'nav > ul.pagination-lg.justify-content-md-start': 1 + 'nav > ul > li.page-item': 10 + 'nav > ul > li.page-item svg': 4 + 'nav > ul > li.page-item > .page-link[aria-label="Ellipsis"]': 2 + 'li.active > div.page-link > span.d-none.d-md-inline-block': 1 + 'li.active > div.page-link > span.d-inline-block.d-md-none': 1 + contains: + 'li.pager__item--prev svg': '/custom-icons.svg#chevron-left' + 'li.pager__item--next svg': '/custom-icons.svg#chevron-right' + 'li.page-item:nth-child(2) svg': '/custom-icons.svg#chevron-double-left' + 'li.page-item:nth-child(9) svg': '/custom-icons.svg#chevron-double-right' + equals: + 'li.active > div.page-link > span.d-none.d-md-inline-block': '6' + 'li.active > div.page-link > span.d-inline-block.d-md-none': 'Page 6 of 10' + 'div.page-link[aria-label="Ellipsis"]': '' + +pagination_v2_custom_item_icon: + render: + '#type': component + '#component': oe_bootstrap_theme:pagination_v2 + '#props': + enable_prev_next_icon: false + items: + - path: '#custom-icon' + label: 'Continue' + aria_label: 'Continue to another page' + icon_position: before + icon: + name: arrow-right + path: /item-icons.svg + transformation: rotate-90 + attributes: + class: + - custom-item-icon + assertions: + count: + 'nav > ul > li.page-item': 1 + 'a.page-link[href="#custom-icon"][aria-label="Continue to another page"] svg.icon--rotate-90.custom-item-icon': 1 + contains: + 'a.page-link svg': '/item-icons.svg#arrow-right' + 'a.page-link': Continue diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/progress.yml b/tests/src/Kernel/fixtures/markup_rendering_components/progress.yml new file mode 100644 index 000000000..18d398274 --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/progress.yml @@ -0,0 +1,98 @@ +progress_primary_striped_animated: + render: + '#type': component + '#component': oe_bootstrap_theme:progress + '#props': + style: primary + striped: true + animated: true + progress: 60 + min: 10 + max: 90 + label: 'Progress label' + text: 'Bar label' + message: 'Progress bar description' + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:progress"]': 1 + 'div.progress-bar': 1 + 'div.progress': 1 + 'div[role="progressbar"]': 1 + 'div[style="width: 60%"]': 1 + 'div[aria-valuenow="60"]': 1 + 'div[aria-valuemin="10"]': 1 + 'div[aria-valuemax="90"]': 1 + 'small.progress-bar-message': 1 + 'div.text-bg-primary': 1 + 'div.progress-bar-animated': 1 + 'div.progress-bar-striped': 1 + contains: + 'label.progress-bar-label': 'Progress label' + 'div.progress': 'Bar label' + 'small.progress-bar-message': 'Progress bar description' +progress_light_non_striped_non_animated: + render: + '#type': component + '#component': oe_bootstrap_theme:progress + '#props': + style: light + striped: false + animated: false + hidden_label: true + progress: 60 + min: 10.5 + max: '90.5' + label: 'Progress label' + text: 'Bar label' + message: 'Progress bar description' + assertions: + count: + 'div[data-component-id="oe_bootstrap_theme:progress"]': 1 + 'div.progress-bar': 1 + 'div.progress': 1 + 'div[role="progressbar"]': 1 + 'div[style="width: 60%"]': 1 + 'div[aria-valuenow="60"]': 1 + 'div[aria-valuemin="10.5"]': 1 + 'div[aria-valuemax="90.5"]': 1 + 'small.progress-bar-message': 1 + 'div.text-bg-light': 1 + 'div.progress-bar-animated': 0 + 'div.progress-bar-striped': 0 + contains: + 'label.progress-bar-label': 'Progress label' + 'small.progress-bar-message': 'Progress bar description' + equals: + 'div.progress-bar': '' +progress_defaults_without_text_or_style: + render: + '#type': component + '#component': oe_bootstrap_theme:progress + '#props': + progress: 33.5 + label: 'Default progress' + assertions: + count: + 'div.progress-bar': 1 + 'div[style="width: 33.5%"]': 1 + 'div[aria-valuenow="33.5"]': 1 + 'div[aria-valuemin="0"]': 1 + 'div[aria-valuemax="100"]': 1 + 'div.progress-bar[class*="text-bg-"]': 0 + equals: + 'label.progress-bar-label': 'Default progress' + 'div.progress-bar': '33.5%' +progress_string_min_number_max: + render: + '#type': component + '#component': oe_bootstrap_theme:progress + '#props': + progress: 50 + min: '5.5' + max: 95.5 + assertions: + count: + 'div[aria-valuemin="5.5"]': 1 + 'div[aria-valuemax="95.5"]': 1 + equals: + 'div.progress-bar': '50%' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/section.yml b/tests/src/Kernel/fixtures/markup_rendering_components/section.yml new file mode 100644 index 000000000..f0c4e56dd --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/section.yml @@ -0,0 +1,95 @@ +section_default: + render: + '#type': component + '#component': oe_bootstrap_theme:section + '#props': + heading: 'This is the heading of the section' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tellus in hac habitasse platea dictumst vestibulum rhoncus est pellentesque.' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\SectionPatternAssert + expected: + tag: section + heading_tag: h2 + heading: 'This is the heading of the section' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tellus in hac habitasse platea dictumst vestibulum rhoncus est pellentesque.' +section_with_custom_attributes: + render: + '#type': component + '#component': oe_bootstrap_theme:section + '#props': + attributes: + class: + - content-wrapper + id: section-id + data-attribute: some-value + wrapper_attributes: + class: + - content-body + id: wrapper-id + data-wrapper-attribute: wrapper-value + heading_attributes: + class: + - content-heading + id: heading-id + data-heading-attribute: heading-value + heading: 'This is the heading of the section' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tellus in hac habitasse platea dictumst vestibulum rhoncus est pellentesque.' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\SectionPatternAssert + expected: + attributes: + class: 'content-wrapper section' + id: section-id + data-attribute: some-value + wrapper_attributes: + class: 'content-body section__body' + id: wrapper-id + data-wrapper-attribute: wrapper-value + heading_attributes: + class: 'content-heading section__title' + id: heading-id + data-heading-attribute: heading-value +section_with_custom_tags: + render: + '#type': component + '#component': oe_bootstrap_theme:section + '#props': + tag: article + heading_tag: h3 + heading: 'This is the heading of the section' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tellus in hac habitasse platea dictumst vestibulum rhoncus est pellentesque.' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\SectionPatternAssert + expected: + tag: article + heading_tag: h3 + heading: 'This is the heading of the section' + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tellus in hac habitasse platea dictumst vestibulum rhoncus est pellentesque.' +section_with_no_title: + render: + '#type': component + '#component': oe_bootstrap_theme:section + '#props': + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tellus in hac habitasse platea dictumst vestibulum rhoncus est pellentesque.' + assertions: + component: + class: Drupal\Tests\oe_bootstrap_theme\PatternAssertion\SectionPatternAssert + expected: + heading: ~ + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tellus in hac habitasse platea dictumst vestibulum rhoncus est pellentesque.' +section_with_no_content: + render: + '#type': component + '#component': oe_bootstrap_theme:section + '#props': + heading: 'This is the heading of the section' + assertions: + count: + '.section > .section__body': 1 + equals: + '.section__body': '' + contains: + '.section__title': 'This is the heading of the section' diff --git a/tests/src/Kernel/fixtures/markup_rendering_components/timeline.yml b/tests/src/Kernel/fixtures/markup_rendering_components/timeline.yml new file mode 100644 index 000000000..f84e4272b --- /dev/null +++ b/tests/src/Kernel/fixtures/markup_rendering_components/timeline.yml @@ -0,0 +1,125 @@ +timeline_items_hide: + render: + '#type': component + '#component': oe_bootstrap_theme:timeline + '#props': + hide_from: 7 + hide_to: 0 + title: 'Title example' + title_tag: 'h3' + title_link: + path: '/timeline' + title_attributes: + class: + - custom-title-class + items: + - id: timeline-item-1 + label: '13 September 2017' + title: 'Item 1' + content: + '#markup': 'Economic and Monetary Union package of proposals' + attributes: + class: + - custom-class + - label: '28-29 September 2017' + title: 'Item 2' + content: 'Lorem Ipsum 2' + - content: 'Lorem Ipsum 3' + - content: 'Lorem Ipsum 4' + - content: 'Lorem Ipsum 5' + - label: '6 February 2018' + title: 'Item 6' + content: 'Lorem Ipsum 6' + assertions: + count: + 'h3.bcl-heading': 1 + 'h3.bcl-heading.custom-title-class a[href="/timeline"]': 1 + 'ol.bcl-timeline': 1 + 'li#timeline-item-1.custom-class': 1 + 'li > p.fs-5.fw-medium': 6 + 'li a': 1 + 'button': 0 + '.collapse': 0 + equals: + 'h3.bcl-heading a': 'Title example' + '.bcl-timeline li:nth-child(1) p.fs-5.fw-medium': '13 September 2017' + '.bcl-timeline li:nth-child(1) p.fs-6.fw-medium': 'Item 1' + '.bcl-timeline li:nth-child(1) div': 'Economic and Monetary Union package of proposals' + '.bcl-timeline li:nth-child(2) p.fs-5.fw-medium': '28-29 September 2017' + '.bcl-timeline li:nth-child(2) p.fs-6.fw-medium': 'Item 2' + '.bcl-timeline li:nth-child(2) div': 'Lorem Ipsum 2' + '.bcl-timeline li:nth-child(3) p.fs-5.fw-medium': '' + '.bcl-timeline li:nth-child(3) div': 'Lorem Ipsum 3' + '.bcl-timeline li:nth-child(4) p.fs-5.fw-medium': '' + '.bcl-timeline li:nth-child(4) div': 'Lorem Ipsum 4' + '.bcl-timeline li:nth-child(5) p.fs-5.fw-medium': '' + '.bcl-timeline li:nth-child(5) div': 'Lorem Ipsum 5' + '.bcl-timeline li:nth-child(6) p.fs-5.fw-medium': '6 February 2018' + '.bcl-timeline li:nth-child(6) p.fs-6.fw-medium': 'Item 6' + '.bcl-timeline li:nth-child(6) div': 'Lorem Ipsum 6' +timeline_items_hide_from_zero: + render: + '#type': component + '#component': oe_bootstrap_theme:timeline + '#props': + hide_from: 0 + items: + - title: 'Item 1' + content: 'Lorem Ipsum 1' + - title: 'Item 2' + content: 'Lorem Ipsum 2' + assertions: + count: + '.bcl-timeline': 1 + '.bcl-timeline > li': 2 + '.bcl-timeline > li.collapse': 0 + '.bcl-timeline > li.bcl-timeline__item--toggle': 0 +timeline_items_show_more: + render: + '#type': component + '#component': oe_bootstrap_theme:timeline + '#props': + collapse_button_label: 'Display %d more' + expand_button_label: 'Stash %d items' + hide_from: 2 + hide_to: 5 + items: + - title: 'Item 1' + content: 'Lorem Ipsum 1' + - title: 'Item 2' + content: 'Lorem Ipsum 2' + - title: 'Item 3' + content: 'Lorem Ipsum 3' + - title: 'Item 4' + content: 'Lorem Ipsum 4' + - title: 'Item 5' + content: 'Lorem Ipsum 5' + - title: 'Item 6' + content: 'Lorem Ipsum 6' + assertions: + count: + '.bcl-timeline': 1 + 'li': 7 + '.label-collapsed': 1 + '.label-expanded': 1 + 'button svg': 1 + '.bcl-timeline > li.collapse': 3 + '.bcl-timeline > li:nth-child(6).bcl-timeline__item--toggle': 1 + '.bcl-timeline > li:nth-child(7).collapse': 0 + contains: + 'button svg': '#chevron-down' + equals: + '.label-expanded': 'Stash 3 items' + '.label-collapsed': 'Display 3 more' + '.bcl-timeline li:nth-child(1) p.fs-6.fw-medium': 'Item 1' + '.bcl-timeline li:nth-child(2) p.fs-6.fw-medium': 'Item 2' + '.bcl-timeline li:nth-child(3) p.fs-6.fw-medium': 'Item 3' + '.bcl-timeline li:nth-child(4) p.fs-6.fw-medium': 'Item 4' + '.bcl-timeline li:nth-child(5) p.fs-6.fw-medium': 'Item 5' + '.bcl-timeline li:nth-child(7) p.fs-6.fw-medium': 'Item 6' + '.bcl-timeline li:nth-child(1) div': 'Lorem Ipsum 1' + '.bcl-timeline li:nth-child(2) div': 'Lorem Ipsum 2' + '.bcl-timeline li:nth-child(3) div': 'Lorem Ipsum 3' + '.bcl-timeline li:nth-child(4) div': 'Lorem Ipsum 4' + '.bcl-timeline li:nth-child(5) div': 'Lorem Ipsum 5' + '.bcl-timeline li:nth-child(7) div': 'Lorem Ipsum 6' diff --git a/tests/src/PatternAssertion/BadgePatternAssert.php b/tests/src/PatternAssertion/BadgePatternAssert.php index 1fb1f1440..e1dd3515c 100644 --- a/tests/src/PatternAssertion/BadgePatternAssert.php +++ b/tests/src/PatternAssertion/BadgePatternAssert.php @@ -20,8 +20,7 @@ protected function getAssertions(string $variant): array { [$this, 'assertSettings'], ], 'label' => [ - [$this, 'assertElementText'], - '.badge', + [$this, 'assertLabel'], ], 'url' => [ [$this, 'assertElementAttribute'], @@ -33,6 +32,9 @@ protected function getAssertions(string $variant): array { '.badge', 'title', ], + 'assistive_text' => [ + [$this, 'assertAssistiveText'], + ], ]; } @@ -56,23 +58,101 @@ protected function assertBaseElements(string $html, string $variant): void { protected function assertSettings(array $expected, Crawler $crawler): void { $background = $expected['background'] ?? 'primary'; - if (!empty($expected['outline'])) { + $this->assertBackground($background, $crawler); + $this->assertOutline($expected['outline'] ?? FALSE, $crawler); + $this->assertRoundedPill($expected['rounded_pill'] ?? FALSE, $crawler); + $this->assertDismissible($expected['dismissible'] ?? FALSE, $crawler); + } + + /** + * Asserts the badge label. + * + * @param string $expected + * The expected label. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The crawler. + */ + protected function assertLabel(string $expected, Crawler $crawler): void { + $this->assertElementExists('.badge', $crawler); + + $badge = $crawler->filter('.badge'); + $actual = trim($badge->text()); + $assistive_text = $badge->filter('.visually-hidden'); + if ($assistive_text->count()) { + $actual = preg_replace('/^' . preg_quote(trim($assistive_text->text()), '/') . '/', '', $actual); + } + + self::assertEquals($expected, trim($actual)); + } + + /** + * Asserts the badge background. + * + * @param string $expected + * The expected background. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The crawler. + */ + protected function assertBackground(string $expected, Crawler $crawler): void { + $this->assertElementExists('.badge-outline-' . $expected . ', .text-bg-' . $expected, $crawler); + } + + /** + * Asserts the badge outline. + * + * @param bool $expected + * Whether the badge should be outlined. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The crawler. + */ + protected function assertOutline(bool $expected, Crawler $crawler): void { + $badge = $crawler->filter('.badge'); + $classes = explode(' ', $badge->attr('class') ?? ''); + $background = 'primary'; + foreach ($classes as $class) { + if (str_starts_with($class, 'badge-outline-') || str_starts_with($class, 'text-bg-')) { + $background = preg_replace('/^(badge-outline|text-bg)-/', '', $class); + break; + } + } + + if ($expected) { $this->assertElementExists('.badge-outline-' . $background, $crawler); - $this->assertElementNotExists('.bg-' . $background, $crawler); + $this->assertElementNotExists('.text-bg-' . $background, $crawler); } else { $this->assertElementNotExists('.badge-outline-' . $background, $crawler); $this->assertElementExists('.text-bg-' . $background, $crawler); } + } - if (!empty($expected['rounded_pill'])) { + /** + * Asserts the badge rounded pill class. + * + * @param bool $expected + * Whether the badge should be a rounded pill. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The crawler. + */ + protected function assertRoundedPill(bool $expected, Crawler $crawler): void { + if ($expected) { $this->assertElementExists('.rounded-pill', $crawler); } else { $this->assertElementNotExists('.rounded-pill', $crawler); } + } - if (!empty($expected['dismissible'])) { + /** + * Asserts the badge dismiss icon. + * + * @param bool $expected + * Whether the badge should be dismissible. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The crawler. + */ + protected function assertDismissible(bool $expected, Crawler $crawler): void { + if ($expected) { $this->assertElementExists('.icon--close', $crawler); } else { @@ -80,4 +160,16 @@ protected function assertSettings(array $expected, Crawler $crawler): void { } } + /** + * Asserts the visually hidden assistive text. + * + * @param string|null $expected + * The expected assistive text. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The crawler. + */ + protected function assertAssistiveText(?string $expected, Crawler $crawler): void { + $this->assertElementText($expected, '.badge .visually-hidden', $crawler); + } + } diff --git a/tests/src/PatternAssertion/BasePatternAssert.php b/tests/src/PatternAssertion/BasePatternAssert.php index 6ab650cf1..3d37aede5 100644 --- a/tests/src/PatternAssertion/BasePatternAssert.php +++ b/tests/src/PatternAssertion/BasePatternAssert.php @@ -4,14 +4,15 @@ namespace Drupal\Tests\oe_bootstrap_theme\PatternAssertion; +use Drupal\Tests\oe_bootstrap_theme\ComponentAssertion\ComponentAssertInterface; use PHPUnit\Framework\Assert; use PHPUnit\Framework\Exception; use Symfony\Component\DomCrawler\Crawler; /** - * Base class for asserting patterns. + * Base class for asserting patterns and SDC components. */ -abstract class BasePatternAssert extends Assert implements PatternAssertInterface { +abstract class BasePatternAssert extends Assert implements PatternAssertInterface, ComponentAssertInterface { /** * Method that returns the assertions to be run by a particular pattern. @@ -83,6 +84,13 @@ public function assertPattern(array $expected, string $html): void { } } + /** + * {@inheritdoc} + */ + public function assertComponent(array $expected, string $html): void { + $this->assertPattern($expected, $html); + } + /** * {@inheritdoc} */ diff --git a/tests/src/PatternAssertion/GalleryPatternAssert.php b/tests/src/PatternAssertion/GalleryPatternAssert.php index f95e9eccd..5e82768f4 100644 --- a/tests/src/PatternAssertion/GalleryPatternAssert.php +++ b/tests/src/PatternAssertion/GalleryPatternAssert.php @@ -82,7 +82,7 @@ protected function assertItems(array $expected, Crawler $crawler): void { $expected_carousel[] = [ 'image' => $item['media']['rendered'], 'caption_title' => $item['media']['caption_title'] ?? NULL, - 'caption' => $item['media']['caption_media'] ?? NULL, + 'caption' => $item['media']['caption'] ?? NULL, ]; }